Skip to content

Commit 73131e9

Browse files
committed
Make BufferingApplicationStartup thread safe
Update `BufferingApplicationStartup` to use thread safe data structures. Prior to this commit, it was possible for calls from different threads (for example due to request scope beans) to cause a NoSuchElementException to be thrown. Closes gh-25792
1 parent fd3d619 commit 73131e9

File tree

5 files changed

+185
-148
lines changed

5 files changed

+185
-148
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,11 +46,10 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
4646
void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) {
4747
StartupStep starting = applicationStartup.start("spring.boot.application.starting");
4848
starting.tag("mainApplicationClass", "com.example.startup.StartupApplication");
49-
starting.end();
50-
5149
StartupStep instantiate = applicationStartup.start("spring.beans.instantiate");
5250
instantiate.tag("beanName", "homeController");
5351
instantiate.end();
52+
starting.end();
5453
}
5554

5655
@Test
@@ -67,14 +66,13 @@ void startup() throws Exception {
6766
fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."),
6867
fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."),
6968
fieldWithPath("timeline.events.[].startupStep.parentId")
70-
.description("The parent id for this StartupStep."),
69+
.description("The parent id for this StartupStep.").optional(),
7170
fieldWithPath("timeline.events.[].startupStep.tags")
7271
.description("An array of key/value pairs with additional step info."),
7372
fieldWithPath("timeline.events.[].startupStep.tags[].key")
7473
.description("The key of the StartupStep Tag."),
7574
fieldWithPath("timeline.events.[].startupStep.tags[].value")
7675
.description("The value of the StartupStep Tag."));
77-
7876
this.mockMvc.perform(post("/actuator/startup")).andExpect(status().isOk())
7977
.andDo(document("startup", responseFields));
8078
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/metrics/buffering/BufferedStartupStep.java

Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,43 +16,54 @@
1616

1717
package org.springframework.boot.context.metrics.buffering;
1818

19-
import java.util.Iterator;
19+
import java.time.Instant;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.concurrent.atomic.AtomicBoolean;
2024
import java.util.function.Consumer;
2125
import java.util.function.Supplier;
2226

2327
import org.springframework.core.metrics.StartupStep;
28+
import org.springframework.util.Assert;
2429

2530
/**
2631
* {@link StartupStep} implementation to be buffered by
2732
* {@link BufferingApplicationStartup}. Its processing time is recorded using
2833
* {@link System#nanoTime()}.
2934
*
3035
* @author Brian Clozel
36+
* @author Phillip Webb
3137
*/
3238
class BufferedStartupStep implements StartupStep {
3339

3440
private final String name;
3541

3642
private final long id;
3743

38-
private final Long parentId;
44+
private final BufferedStartupStep parent;
3945

40-
private long startTime;
46+
private final List<Tag> tags = new ArrayList<>();
4147

42-
private long endTime;
48+
private final Consumer<BufferedStartupStep> recorder;
4349

44-
private final DefaultTags tags;
50+
private final Instant startTime;
4551

46-
private final Consumer<BufferedStartupStep> recorder;
52+
private final AtomicBoolean ended = new AtomicBoolean();
4753

48-
BufferedStartupStep(long id, String name, Long parentId, Consumer<BufferedStartupStep> recorder) {
49-
this.id = id;
50-
this.parentId = parentId;
51-
this.tags = new DefaultTags();
54+
BufferedStartupStep(BufferedStartupStep parent, String name, long id, Instant startTime,
55+
Consumer<BufferedStartupStep> recorder) {
56+
this.parent = parent;
5257
this.name = name;
58+
this.id = id;
59+
this.startTime = startTime;
5360
this.recorder = recorder;
5461
}
5562

63+
BufferedStartupStep getParent() {
64+
return this.parent;
65+
}
66+
5667
@Override
5768
public String getName() {
5869
return this.name;
@@ -63,88 +74,40 @@ public long getId() {
6374
return this.id;
6475
}
6576

77+
Instant getStartTime() {
78+
return this.startTime;
79+
}
80+
6681
@Override
6782
public Long getParentId() {
68-
return this.parentId;
83+
return (this.parent != null) ? this.parent.getId() : null;
6984
}
7085

7186
@Override
7287
public Tags getTags() {
73-
return this.tags;
88+
return Collections.unmodifiableList(this.tags)::iterator;
7489
}
7590

7691
@Override
77-
public StartupStep tag(String key, String value) {
78-
if (this.endTime != 0L) {
79-
throw new IllegalStateException("StartupStep has already ended.");
80-
}
81-
this.tags.add(key, value);
82-
return this;
92+
public StartupStep tag(String key, Supplier<String> value) {
93+
return this.tag(key, value.get());
8394
}
8495

8596
@Override
86-
public StartupStep tag(String key, Supplier<String> value) {
87-
return this.tag(key, value.get());
97+
public StartupStep tag(String key, String value) {
98+
Assert.state(!this.ended.get(), "StartupStep has already ended.");
99+
this.tags.add(new DefaultTag(key, value));
100+
return this;
88101
}
89102

90103
@Override
91104
public void end() {
105+
this.ended.set(true);
92106
this.recorder.accept(this);
93107
}
94108

95-
long getStartTime() {
96-
return this.startTime;
97-
}
98-
99-
void recordStartTime(long startTime) {
100-
this.startTime = startTime;
101-
}
102-
103-
long getEndTime() {
104-
return this.endTime;
105-
}
106-
107-
void recordEndTime(long endTime) {
108-
this.endTime = endTime;
109-
}
110-
111-
static class DefaultTags implements Tags {
112-
113-
private Tag[] tags = new Tag[0];
114-
115-
void add(String key, String value) {
116-
Tag[] newTags = new Tag[this.tags.length + 1];
117-
System.arraycopy(this.tags, 0, newTags, 0, this.tags.length);
118-
newTags[newTags.length - 1] = new DefaultTag(key, value);
119-
this.tags = newTags;
120-
}
121-
122-
@Override
123-
public Iterator<Tag> iterator() {
124-
return new TagsIterator();
125-
}
126-
127-
private class TagsIterator implements Iterator<Tag> {
128-
129-
private int index = 0;
130-
131-
@Override
132-
public boolean hasNext() {
133-
return this.index < DefaultTags.this.tags.length;
134-
}
135-
136-
@Override
137-
public Tag next() {
138-
return DefaultTags.this.tags[this.index++];
139-
}
140-
141-
@Override
142-
public void remove() {
143-
throw new UnsupportedOperationException("tags are append only");
144-
}
145-
146-
}
147-
109+
boolean isEnded() {
110+
return this.ended.get();
148111
}
149112

150113
static class DefaultTag implements Tag {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,15 +16,17 @@
1616

1717
package org.springframework.boot.context.metrics.buffering;
1818

19+
import java.time.Clock;
1920
import java.time.Instant;
20-
import java.util.ArrayDeque;
2121
import java.util.ArrayList;
22-
import java.util.Deque;
22+
import java.util.Iterator;
2323
import java.util.List;
24-
import java.util.concurrent.BlockingQueue;
25-
import java.util.concurrent.LinkedBlockingQueue;
24+
import java.util.concurrent.ConcurrentLinkedQueue;
25+
import java.util.concurrent.atomic.AtomicInteger;
26+
import java.util.concurrent.atomic.AtomicReference;
2627
import java.util.function.Predicate;
2728

29+
import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
2830
import org.springframework.core.metrics.ApplicationStartup;
2931
import org.springframework.core.metrics.StartupStep;
3032
import org.springframework.util.Assert;
@@ -45,32 +47,40 @@
4547
* </ul>
4648
*
4749
* @author Brian Clozel
50+
* @author Phillip Webb
4851
* @since 2.4.0
4952
*/
5053
public class BufferingApplicationStartup implements ApplicationStartup {
5154

52-
private Instant recordingStartTime;
55+
private final int capacity;
5356

54-
private long recordingStartNanos;
57+
private final Clock clock;
5558

56-
private long currentSequenceId = 0;
59+
private Instant startTime;
5760

58-
private final Deque<Long> currentSteps;
61+
private final AtomicInteger idSeq = new AtomicInteger();
5962

60-
private final BlockingQueue<BufferedStartupStep> recordedSteps;
63+
private Predicate<StartupStep> filter = (step) -> true;
6164

62-
private Predicate<StartupStep> stepFilters = (step) -> true;
65+
private final AtomicReference<BufferedStartupStep> current = new AtomicReference<>();
66+
67+
private final AtomicInteger estimatedSize = new AtomicInteger();
68+
69+
private final ConcurrentLinkedQueue<TimelineEvent> events = new ConcurrentLinkedQueue<>();
6370

6471
/**
6572
* Create a new buffered {@link ApplicationStartup} with a limited capacity and starts
6673
* the recording of steps.
6774
* @param capacity the configured capacity; once reached, new steps are not recorded.
6875
*/
6976
public BufferingApplicationStartup(int capacity) {
70-
this.currentSteps = new ArrayDeque<>();
71-
this.currentSteps.offerFirst(this.currentSequenceId);
72-
this.recordedSteps = new LinkedBlockingQueue<>(capacity);
73-
startRecording();
77+
this(capacity, Clock.systemDefaultZone());
78+
}
79+
80+
BufferingApplicationStartup(int capacity, Clock clock) {
81+
this.capacity = capacity;
82+
this.clock = clock;
83+
this.startTime = clock.instant();
7484
}
7585

7686
/**
@@ -81,9 +91,8 @@ public BufferingApplicationStartup(int capacity) {
8191
* already.
8292
*/
8393
public void startRecording() {
84-
Assert.state(this.recordedSteps.isEmpty(), "Cannot restart recording once steps have been buffered.");
85-
this.recordingStartTime = Instant.now();
86-
this.recordingStartNanos = getCurrentTime();
94+
Assert.state(this.events.isEmpty(), "Cannot restart recording once steps have been buffered.");
95+
this.startTime = this.clock.instant();
8796
}
8897

8998
/**
@@ -93,7 +102,42 @@ public void startRecording() {
93102
* @param filter the predicate filter to add.
94103
*/
95104
public void addFilter(Predicate<StartupStep> filter) {
96-
this.stepFilters = this.stepFilters.and(filter);
105+
this.filter = this.filter.and(filter);
106+
}
107+
108+
@Override
109+
public StartupStep start(String name) {
110+
int id = this.idSeq.getAndIncrement();
111+
Instant start = this.clock.instant();
112+
while (true) {
113+
BufferedStartupStep current = this.current.get();
114+
BufferedStartupStep parent = getLatestActive(current);
115+
BufferedStartupStep next = new BufferedStartupStep(parent, name, id, start, this::record);
116+
if (this.current.compareAndSet(current, next)) {
117+
return next;
118+
}
119+
}
120+
}
121+
122+
private void record(BufferedStartupStep step) {
123+
if (this.filter.test(step) && this.estimatedSize.get() < this.capacity) {
124+
this.estimatedSize.incrementAndGet();
125+
this.events.add(new TimelineEvent(step, this.clock.instant()));
126+
}
127+
while (true) {
128+
BufferedStartupStep current = this.current.get();
129+
BufferedStartupStep next = getLatestActive(current);
130+
if (this.current.compareAndSet(current, next)) {
131+
return;
132+
}
133+
}
134+
}
135+
136+
private BufferedStartupStep getLatestActive(BufferedStartupStep step) {
137+
while (step != null && step.isEnded()) {
138+
step = step.getParent();
139+
}
140+
return step;
97141
}
98142

99143
/**
@@ -105,7 +149,7 @@ public void addFilter(Predicate<StartupStep> filter) {
105149
* @return a snapshot of currently buffered steps.
106150
*/
107151
public StartupTimeline getBufferedTimeline() {
108-
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, this.recordedSteps);
152+
return new StartupTimeline(this.startTime, new ArrayList<>(this.events));
109153
}
110154

111155
/**
@@ -116,30 +160,14 @@ public StartupTimeline getBufferedTimeline() {
116160
* @return buffered steps drained from the buffer.
117161
*/
118162
public StartupTimeline drainBufferedTimeline() {
119-
List<BufferedStartupStep> steps = new ArrayList<>(this.recordedSteps.size());
120-
this.recordedSteps.drainTo(steps);
121-
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, steps);
122-
}
123-
124-
@Override
125-
public StartupStep start(String name) {
126-
BufferedStartupStep step = new BufferedStartupStep(++this.currentSequenceId, name,
127-
this.currentSteps.peekFirst(), this::record);
128-
step.recordStartTime(getCurrentTime());
129-
this.currentSteps.offerFirst(this.currentSequenceId);
130-
return step;
131-
}
132-
133-
private void record(BufferedStartupStep step) {
134-
step.recordEndTime(getCurrentTime());
135-
if (this.stepFilters.test(step)) {
136-
this.recordedSteps.offer(step);
163+
List<TimelineEvent> events = new ArrayList<>();
164+
Iterator<TimelineEvent> iterator = this.events.iterator();
165+
while (iterator.hasNext()) {
166+
events.add(iterator.next());
167+
iterator.remove();
137168
}
138-
this.currentSteps.removeFirst();
139-
}
140-
141-
private long getCurrentTime() {
142-
return System.nanoTime();
169+
this.estimatedSize.set(0);
170+
return new StartupTimeline(this.startTime, events);
143171
}
144172

145173
}

0 commit comments

Comments
 (0)