Commit 83668f96 authored by Phillip Webb's avatar Phillip Webb

Merge branch '2.4.x'

Closes gh-26458
parents f44c99df 73131e99
...@@ -48,11 +48,10 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest ...@@ -48,11 +48,10 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) { void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) {
StartupStep starting = applicationStartup.start("spring.boot.application.starting"); StartupStep starting = applicationStartup.start("spring.boot.application.starting");
starting.tag("mainApplicationClass", "com.example.startup.StartupApplication"); starting.tag("mainApplicationClass", "com.example.startup.StartupApplication");
starting.end();
StartupStep instantiate = applicationStartup.start("spring.beans.instantiate"); StartupStep instantiate = applicationStartup.start("spring.beans.instantiate");
instantiate.tag("beanName", "homeController"); instantiate.tag("beanName", "homeController");
instantiate.end(); instantiate.end();
starting.end();
} }
@Test @Test
...@@ -80,7 +79,7 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest ...@@ -80,7 +79,7 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."), fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."),
fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."), fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."),
fieldWithPath("timeline.events.[].startupStep.parentId") fieldWithPath("timeline.events.[].startupStep.parentId")
.description("The parent id for this StartupStep."), .description("The parent id for this StartupStep.").optional(),
fieldWithPath("timeline.events.[].startupStep.tags") fieldWithPath("timeline.events.[].startupStep.tags")
.description("An array of key/value pairs with additional step info."), .description("An array of key/value pairs with additional step info."),
fieldWithPath("timeline.events.[].startupStep.tags[].key") fieldWithPath("timeline.events.[].startupStep.tags[].key")
......
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,11 +16,16 @@ ...@@ -16,11 +16,16 @@
package org.springframework.boot.context.metrics.buffering; package org.springframework.boot.context.metrics.buffering;
import java.util.Iterator; import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.core.metrics.StartupStep; import org.springframework.core.metrics.StartupStep;
import org.springframework.util.Assert;
/** /**
* {@link StartupStep} implementation to be buffered by * {@link StartupStep} implementation to be buffered by
...@@ -28,6 +33,7 @@ import org.springframework.core.metrics.StartupStep; ...@@ -28,6 +33,7 @@ import org.springframework.core.metrics.StartupStep;
* {@link System#nanoTime()}. * {@link System#nanoTime()}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Phillip Webb
*/ */
class BufferedStartupStep implements StartupStep { class BufferedStartupStep implements StartupStep {
...@@ -35,24 +41,29 @@ class BufferedStartupStep implements StartupStep { ...@@ -35,24 +41,29 @@ class BufferedStartupStep implements StartupStep {
private final long id; private final long id;
private final Long parentId; private final BufferedStartupStep parent;
private long startTime; private final List<Tag> tags = new ArrayList<>();
private long endTime; private final Consumer<BufferedStartupStep> recorder;
private final DefaultTags tags; private final Instant startTime;
private final Consumer<BufferedStartupStep> recorder; private final AtomicBoolean ended = new AtomicBoolean();
BufferedStartupStep(long id, String name, Long parentId, Consumer<BufferedStartupStep> recorder) { BufferedStartupStep(BufferedStartupStep parent, String name, long id, Instant startTime,
this.id = id; Consumer<BufferedStartupStep> recorder) {
this.parentId = parentId; this.parent = parent;
this.tags = new DefaultTags();
this.name = name; this.name = name;
this.id = id;
this.startTime = startTime;
this.recorder = recorder; this.recorder = recorder;
} }
BufferedStartupStep getParent() {
return this.parent;
}
@Override @Override
public String getName() { public String getName() {
return this.name; return this.name;
...@@ -63,88 +74,40 @@ class BufferedStartupStep implements StartupStep { ...@@ -63,88 +74,40 @@ class BufferedStartupStep implements StartupStep {
return this.id; return this.id;
} }
Instant getStartTime() {
return this.startTime;
}
@Override @Override
public Long getParentId() { public Long getParentId() {
return this.parentId; return (this.parent != null) ? this.parent.getId() : null;
} }
@Override @Override
public Tags getTags() { public Tags getTags() {
return this.tags; return Collections.unmodifiableList(this.tags)::iterator;
} }
@Override @Override
public StartupStep tag(String key, String value) { public StartupStep tag(String key, Supplier<String> value) {
if (this.endTime != 0L) { return this.tag(key, value.get());
throw new IllegalStateException("StartupStep has already ended.");
}
this.tags.add(key, value);
return this;
} }
@Override @Override
public StartupStep tag(String key, Supplier<String> value) { public StartupStep tag(String key, String value) {
return this.tag(key, value.get()); Assert.state(!this.ended.get(), "StartupStep has already ended.");
this.tags.add(new DefaultTag(key, value));
return this;
} }
@Override @Override
public void end() { public void end() {
this.ended.set(true);
this.recorder.accept(this); this.recorder.accept(this);
} }
long getStartTime() { boolean isEnded() {
return this.startTime; return this.ended.get();
}
void recordStartTime(long startTime) {
this.startTime = startTime;
}
long getEndTime() {
return this.endTime;
}
void recordEndTime(long endTime) {
this.endTime = endTime;
}
static class DefaultTags implements Tags {
private Tag[] tags = new Tag[0];
void add(String key, String value) {
Tag[] newTags = new Tag[this.tags.length + 1];
System.arraycopy(this.tags, 0, newTags, 0, this.tags.length);
newTags[newTags.length - 1] = new DefaultTag(key, value);
this.tags = newTags;
}
@Override
public Iterator<Tag> iterator() {
return new TagsIterator();
}
private class TagsIterator implements Iterator<Tag> {
private int index = 0;
@Override
public boolean hasNext() {
return this.index < DefaultTags.this.tags.length;
}
@Override
public Tag next() {
return DefaultTags.this.tags[this.index++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("tags are append only");
}
}
} }
static class DefaultTag implements Tag { static class DefaultTag implements Tag {
......
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,15 +16,17 @@ ...@@ -16,15 +16,17 @@
package org.springframework.boot.context.metrics.buffering; package org.springframework.boot.context.metrics.buffering;
import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.metrics.StartupStep; import org.springframework.core.metrics.StartupStep;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -45,21 +47,26 @@ import org.springframework.util.Assert; ...@@ -45,21 +47,26 @@ import org.springframework.util.Assert;
* </ul> * </ul>
* *
* @author Brian Clozel * @author Brian Clozel
* @author Phillip Webb
* @since 2.4.0 * @since 2.4.0
*/ */
public class BufferingApplicationStartup implements ApplicationStartup { public class BufferingApplicationStartup implements ApplicationStartup {
private Instant recordingStartTime; private final int capacity;
private long recordingStartNanos; private final Clock clock;
private long currentSequenceId = 0; private Instant startTime;
private final Deque<Long> currentSteps; private final AtomicInteger idSeq = new AtomicInteger();
private final BlockingQueue<BufferedStartupStep> recordedSteps; private Predicate<StartupStep> filter = (step) -> true;
private Predicate<StartupStep> stepFilters = (step) -> true; private final AtomicReference<BufferedStartupStep> current = new AtomicReference<>();
private final AtomicInteger estimatedSize = new AtomicInteger();
private final ConcurrentLinkedQueue<TimelineEvent> events = new ConcurrentLinkedQueue<>();
/** /**
* Create a new buffered {@link ApplicationStartup} with a limited capacity and starts * Create a new buffered {@link ApplicationStartup} with a limited capacity and starts
...@@ -67,10 +74,13 @@ public class BufferingApplicationStartup implements ApplicationStartup { ...@@ -67,10 +74,13 @@ public class BufferingApplicationStartup implements ApplicationStartup {
* @param capacity the configured capacity; once reached, new steps are not recorded. * @param capacity the configured capacity; once reached, new steps are not recorded.
*/ */
public BufferingApplicationStartup(int capacity) { public BufferingApplicationStartup(int capacity) {
this.currentSteps = new ArrayDeque<>(); this(capacity, Clock.systemDefaultZone());
this.currentSteps.offerFirst(this.currentSequenceId); }
this.recordedSteps = new LinkedBlockingQueue<>(capacity);
startRecording(); BufferingApplicationStartup(int capacity, Clock clock) {
this.capacity = capacity;
this.clock = clock;
this.startTime = clock.instant();
} }
/** /**
...@@ -81,9 +91,8 @@ public class BufferingApplicationStartup implements ApplicationStartup { ...@@ -81,9 +91,8 @@ public class BufferingApplicationStartup implements ApplicationStartup {
* already. * already.
*/ */
public void startRecording() { public void startRecording() {
Assert.state(this.recordedSteps.isEmpty(), "Cannot restart recording once steps have been buffered."); Assert.state(this.events.isEmpty(), "Cannot restart recording once steps have been buffered.");
this.recordingStartTime = Instant.now(); this.startTime = this.clock.instant();
this.recordingStartNanos = getCurrentTime();
} }
/** /**
...@@ -93,7 +102,42 @@ public class BufferingApplicationStartup implements ApplicationStartup { ...@@ -93,7 +102,42 @@ public class BufferingApplicationStartup implements ApplicationStartup {
* @param filter the predicate filter to add. * @param filter the predicate filter to add.
*/ */
public void addFilter(Predicate<StartupStep> filter) { public void addFilter(Predicate<StartupStep> filter) {
this.stepFilters = this.stepFilters.and(filter); this.filter = this.filter.and(filter);
}
@Override
public StartupStep start(String name) {
int id = this.idSeq.getAndIncrement();
Instant start = this.clock.instant();
while (true) {
BufferedStartupStep current = this.current.get();
BufferedStartupStep parent = getLatestActive(current);
BufferedStartupStep next = new BufferedStartupStep(parent, name, id, start, this::record);
if (this.current.compareAndSet(current, next)) {
return next;
}
}
}
private void record(BufferedStartupStep step) {
if (this.filter.test(step) && this.estimatedSize.get() < this.capacity) {
this.estimatedSize.incrementAndGet();
this.events.add(new TimelineEvent(step, this.clock.instant()));
}
while (true) {
BufferedStartupStep current = this.current.get();
BufferedStartupStep next = getLatestActive(current);
if (this.current.compareAndSet(current, next)) {
return;
}
}
}
private BufferedStartupStep getLatestActive(BufferedStartupStep step) {
while (step != null && step.isEnded()) {
step = step.getParent();
}
return step;
} }
/** /**
...@@ -105,7 +149,7 @@ public class BufferingApplicationStartup implements ApplicationStartup { ...@@ -105,7 +149,7 @@ public class BufferingApplicationStartup implements ApplicationStartup {
* @return a snapshot of currently buffered steps. * @return a snapshot of currently buffered steps.
*/ */
public StartupTimeline getBufferedTimeline() { public StartupTimeline getBufferedTimeline() {
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, this.recordedSteps); return new StartupTimeline(this.startTime, new ArrayList<>(this.events));
} }
/** /**
...@@ -116,30 +160,14 @@ public class BufferingApplicationStartup implements ApplicationStartup { ...@@ -116,30 +160,14 @@ public class BufferingApplicationStartup implements ApplicationStartup {
* @return buffered steps drained from the buffer. * @return buffered steps drained from the buffer.
*/ */
public StartupTimeline drainBufferedTimeline() { public StartupTimeline drainBufferedTimeline() {
List<BufferedStartupStep> steps = new ArrayList<>(this.recordedSteps.size()); List<TimelineEvent> events = new ArrayList<>();
this.recordedSteps.drainTo(steps); Iterator<TimelineEvent> iterator = this.events.iterator();
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, steps); while (iterator.hasNext()) {
} events.add(iterator.next());
iterator.remove();
@Override
public StartupStep start(String name) {
BufferedStartupStep step = new BufferedStartupStep(++this.currentSequenceId, name,
this.currentSteps.peekFirst(), this::record);
step.recordStartTime(getCurrentTime());
this.currentSteps.offerFirst(this.currentSequenceId);
return step;
}
private void record(BufferedStartupStep step) {
step.recordEndTime(getCurrentTime());
if (this.stepFilters.test(step)) {
this.recordedSteps.offer(step);
} }
this.currentSteps.removeFirst(); this.estimatedSize.set(0);
} return new StartupTimeline(this.startTime, events);
private long getCurrentTime() {
return System.nanoTime();
} }
} }
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,9 +18,8 @@ package org.springframework.boot.context.metrics.buffering; ...@@ -18,9 +18,8 @@ package org.springframework.boot.context.metrics.buffering;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.core.metrics.StartupStep; import org.springframework.core.metrics.StartupStep;
...@@ -38,10 +37,9 @@ public class StartupTimeline { ...@@ -38,10 +37,9 @@ public class StartupTimeline {
private final List<TimelineEvent> events; private final List<TimelineEvent> events;
StartupTimeline(Instant startTime, long startNanoTime, Collection<BufferedStartupStep> events) { StartupTimeline(Instant startTime, List<TimelineEvent> events) {
this.startTime = startTime; this.startTime = startTime;
this.events = events.stream().map((event) -> new TimelineEvent(event, startTime, startNanoTime)) this.events = Collections.unmodifiableList(events);
.collect(Collectors.toList());
} }
/** /**
...@@ -67,19 +65,16 @@ public class StartupTimeline { ...@@ -67,19 +65,16 @@ public class StartupTimeline {
*/ */
public static class TimelineEvent { public static class TimelineEvent {
private final StartupStep startupStep; private final BufferedStartupStep step;
private final Instant startTime;
private final Instant endTime; private final Instant endTime;
private final Duration duration; private final Duration duration;
TimelineEvent(BufferedStartupStep startupStep, Instant startupDate, long startupNanoTime) { TimelineEvent(BufferedStartupStep step, Instant endTime) {
this.startupStep = startupStep; this.step = step;
this.startTime = startupDate.plus(Duration.ofNanos(startupStep.getStartTime() - startupNanoTime)); this.endTime = endTime;
this.endTime = startupDate.plus(Duration.ofNanos(startupStep.getEndTime() - startupNanoTime)); this.duration = Duration.between(step.getStartTime(), endTime);
this.duration = Duration.ofNanos(startupStep.getEndTime() - startupStep.getStartTime());
} }
/** /**
...@@ -87,7 +82,7 @@ public class StartupTimeline { ...@@ -87,7 +82,7 @@ public class StartupTimeline {
* @return the start time * @return the start time
*/ */
public Instant getStartTime() { public Instant getStartTime() {
return this.startTime; return this.step.getStartTime();
} }
/** /**
...@@ -112,7 +107,7 @@ public class StartupTimeline { ...@@ -112,7 +107,7 @@ public class StartupTimeline {
* @return the step information. * @return the step information.
*/ */
public StartupStep getStartupStep() { public StartupStep getStartupStep() {
return this.startupStep; return this.step;
} }
} }
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,8 +16,14 @@ ...@@ -16,8 +16,14 @@
package org.springframework.boot.context.metrics.buffering; package org.springframework.boot.context.metrics.buffering;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.context.metrics.buffering.StartupTimeline.TimelineEvent;
import org.springframework.core.metrics.StartupStep; import org.springframework.core.metrics.StartupStep;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -27,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; ...@@ -27,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* Tests for {@link BufferingApplicationStartup}. * Tests for {@link BufferingApplicationStartup}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Phillip Webb
*/ */
class BufferingApplicationStartupTests { class BufferingApplicationStartupTests {
...@@ -47,13 +54,14 @@ class BufferingApplicationStartupTests { ...@@ -47,13 +54,14 @@ class BufferingApplicationStartupTests {
StartupStep filtered = applicationStartup.start("filtered.second"); StartupStep filtered = applicationStartup.start("filtered.second");
applicationStartup.start("spring.third").end(); applicationStartup.start("spring.third").end();
filtered.end(); filtered.end();
assertThat(applicationStartup.getBufferedTimeline().getEvents()).hasSize(2); List<TimelineEvent> events = applicationStartup.getBufferedTimeline().getEvents();
StartupTimeline.TimelineEvent firstEvent = applicationStartup.getBufferedTimeline().getEvents().get(0); assertThat(events).hasSize(2);
assertThat(firstEvent.getStartupStep().getId()).isEqualTo(1); StartupTimeline.TimelineEvent firstEvent = events.get(0);
assertThat(firstEvent.getStartupStep().getParentId()).isEqualTo(0); assertThat(firstEvent.getStartupStep().getId()).isEqualTo(0);
StartupTimeline.TimelineEvent secondEvent = applicationStartup.getBufferedTimeline().getEvents().get(1); assertThat(firstEvent.getStartupStep().getParentId()).isNull();
assertThat(secondEvent.getStartupStep().getId()).isEqualTo(3); StartupTimeline.TimelineEvent secondEvent = events.get(1);
assertThat(secondEvent.getStartupStep().getParentId()).isEqualTo(2); assertThat(secondEvent.getStartupStep().getId()).isEqualTo(2);
assertThat(secondEvent.getStartupStep().getParentId()).isEqualTo(1);
} }
@Test @Test
...@@ -96,8 +104,53 @@ class BufferingApplicationStartupTests { ...@@ -96,8 +104,53 @@ class BufferingApplicationStartupTests {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(2); BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(2);
StartupStep step = applicationStartup.start("first"); StartupStep step = applicationStartup.start("first");
step.tag("name", "value"); step.tag("name", "value");
assertThatThrownBy(() -> step.getTags().iterator().remove()).isInstanceOf(UnsupportedOperationException.class) assertThatThrownBy(() -> step.getTags().iterator().remove()).isInstanceOf(UnsupportedOperationException.class);
.hasMessage("tags are append only"); }
@Test // gh-25792
void outOfOrderWithMultipleEndCallsShouldNotFail() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(200);
StartupStep one = applicationStartup.start("one");
StartupStep two = applicationStartup.start("two");
StartupStep three = applicationStartup.start("three");
two.end();
two.end();
two.end();
StartupStep four = applicationStartup.start("four");
four.end();
three.end();
one.end();
}
@Test // gh-25792
void multiThreadedAccessShouldWork() throws InterruptedException {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(5000);
Queue<Exception> errors = new ConcurrentLinkedQueue<>();
List<Thread> threads = new ArrayList<>();
for (int thread = 0; thread < 20; thread++) {
String prefix = "thread-" + thread + "-";
threads.add(new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
StartupStep step = applicationStartup.start(prefix + i);
try {
Thread.sleep(1);
}
catch (InterruptedException ex) {
}
step.end();
}
}
catch (Exception ex) {
errors.add(ex);
}
}));
}
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
assertThat(errors).isEmpty();
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment