Browse Source
This commit enhances the `ScheduledAnnotationBeanPostProcessor` to instrument `@Scheduled` methods declared on beans. This will create `"tasks.scheduled.execution"` observations for each execution of a scheduled method. This supports both blocking and reactive variants. By default, observations are no-ops; developers must configure the current `ObservationRegistry` on the `ScheduledTaskRegistrar` by using a `SchedulingConfigurer`. Closes gh-29883pull/30690/head
14 changed files with 899 additions and 23 deletions
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.docs.integration.observability.tasksscheduled; |
||||
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry; |
||||
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer; |
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar; |
||||
|
||||
public class ObservationSchedulingConfigurer implements SchedulingConfigurer { |
||||
|
||||
private final ObservationRegistry observationRegistry; |
||||
|
||||
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) { |
||||
this.observationRegistry = observationRegistry; |
||||
} |
||||
|
||||
@Override |
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { |
||||
taskRegistrar.setObservationRegistry(this.observationRegistry); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.config; |
||||
|
||||
import io.micrometer.common.KeyValue; |
||||
import io.micrometer.common.KeyValues; |
||||
|
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import static org.springframework.scheduling.config.ScheduledTaskObservationDocumentation.LowCardinalityKeyNames; |
||||
|
||||
/** |
||||
* Default implementation for {@link ScheduledTaskObservationConvention}. |
||||
* @author Brian Clozel |
||||
* @since 6.1.0 |
||||
*/ |
||||
public class DefaultScheduledTaskObservationConvention implements ScheduledTaskObservationConvention { |
||||
|
||||
private static final String DEFAULT_NAME = "tasks.scheduled.execution"; |
||||
|
||||
private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); |
||||
|
||||
private static final KeyValue OUTCOME_SUCCESS = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "SUCCESS"); |
||||
|
||||
private static final KeyValue OUTCOME_ERROR = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "ERROR"); |
||||
|
||||
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN"); |
||||
|
||||
@Override |
||||
public String getName() { |
||||
return DEFAULT_NAME; |
||||
} |
||||
|
||||
@Override |
||||
public String getContextualName(ScheduledTaskObservationContext context) { |
||||
return "task " + StringUtils.uncapitalize(context.getTargetClass().getSimpleName()) |
||||
+ "." + context.getMethod().getName(); |
||||
} |
||||
|
||||
@Override |
||||
public KeyValues getLowCardinalityKeyValues(ScheduledTaskObservationContext context) { |
||||
return KeyValues.of(exception(context), methodName(context), outcome(context), targetType(context)); |
||||
} |
||||
|
||||
protected KeyValue exception(ScheduledTaskObservationContext context) { |
||||
if (context.getError() != null) { |
||||
return KeyValue.of(LowCardinalityKeyNames.EXCEPTION, context.getError().getClass().getSimpleName()); |
||||
} |
||||
return EXCEPTION_NONE; |
||||
} |
||||
|
||||
protected KeyValue methodName(ScheduledTaskObservationContext context) { |
||||
return KeyValue.of(LowCardinalityKeyNames.METHOD_NAME, context.getMethod().getName()); |
||||
} |
||||
|
||||
protected KeyValue outcome(ScheduledTaskObservationContext context) { |
||||
if (context.getError() != null) { |
||||
return OUTCOME_ERROR; |
||||
} |
||||
else if (!context.isComplete()) { |
||||
return OUTCOME_UNKNOWN; |
||||
} |
||||
return OUTCOME_SUCCESS; |
||||
} |
||||
|
||||
protected KeyValue targetType(ScheduledTaskObservationContext context) { |
||||
return KeyValue.of(LowCardinalityKeyNames.TARGET_TYPE, context.getTargetClass().getSimpleName()); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.config; |
||||
|
||||
import java.lang.reflect.Method; |
||||
|
||||
import io.micrometer.observation.Observation; |
||||
|
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
/** |
||||
* Context that holds information for observation metadata collection |
||||
* during the {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION execution of scheduled tasks}. |
||||
* @author Brian Clozel |
||||
* @since 6.1.0 |
||||
*/ |
||||
public class ScheduledTaskObservationContext extends Observation.Context { |
||||
|
||||
private final Class<?> targetClass; |
||||
|
||||
private final Method method; |
||||
|
||||
private boolean complete; |
||||
|
||||
/** |
||||
* Create a new observation context for a task, given the target object |
||||
* and the method to be called. |
||||
* @param target the target object that is called for task execution |
||||
* @param method the method that is called for task execution |
||||
*/ |
||||
public ScheduledTaskObservationContext(Object target, Method method) { |
||||
this.targetClass = ClassUtils.getUserClass(target); |
||||
this.method = method; |
||||
} |
||||
|
||||
/** |
||||
* Return the type of the target object. |
||||
*/ |
||||
public Class<?> getTargetClass() { |
||||
return this.targetClass; |
||||
} |
||||
|
||||
/** |
||||
* Return the method that is called for task execution. |
||||
*/ |
||||
public Method getMethod() { |
||||
return this.method; |
||||
} |
||||
|
||||
/** |
||||
* Return whether the task execution is complete. |
||||
* <p>If an observation has ended and the task is not complete, this means |
||||
* that an {@link #getError() error} was raised or that the task execution got cancelled |
||||
* during its execution. |
||||
*/ |
||||
public boolean isComplete() { |
||||
return this.complete; |
||||
} |
||||
|
||||
/** |
||||
* Set whether the task execution has completed. |
||||
*/ |
||||
public void setComplete(boolean complete) { |
||||
this.complete = complete; |
||||
} |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.config; |
||||
|
||||
import io.micrometer.observation.Observation; |
||||
import io.micrometer.observation.ObservationConvention; |
||||
|
||||
/** |
||||
* Interface for an {@link ObservationConvention} for |
||||
* {@link ScheduledTaskObservationDocumentation#TASKS_SCHEDULED_EXECUTION scheduled task executions}. |
||||
* @author Brian Clozel |
||||
* @since 6.1.0 |
||||
*/ |
||||
public interface ScheduledTaskObservationConvention extends ObservationConvention<ScheduledTaskObservationContext> { |
||||
|
||||
@Override |
||||
default boolean supportsContext(Observation.Context context) { |
||||
return context instanceof ScheduledTaskObservationContext; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.config; |
||||
|
||||
import io.micrometer.common.KeyValue; |
||||
import io.micrometer.common.docs.KeyName; |
||||
import io.micrometer.observation.Observation; |
||||
import io.micrometer.observation.ObservationConvention; |
||||
import io.micrometer.observation.docs.ObservationDocumentation; |
||||
|
||||
/** |
||||
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the observations on |
||||
* executions of {@link org.springframework.scheduling.annotation.Scheduled scheduled tasks}. |
||||
* <p>This class is used by automated tools to document KeyValues attached to the {@code @Scheduled} observations. |
||||
* |
||||
* @author Brian Clozel |
||||
* @since 6.1.0 |
||||
*/ |
||||
public enum ScheduledTaskObservationDocumentation implements ObservationDocumentation { |
||||
|
||||
/** |
||||
* Observations on executions of {@link org.springframework.scheduling.annotation.Scheduled} tasks. |
||||
*/ |
||||
TASKS_SCHEDULED_EXECUTION { |
||||
@Override |
||||
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() { |
||||
return DefaultScheduledTaskObservationConvention.class; |
||||
} |
||||
|
||||
@Override |
||||
public KeyName[] getLowCardinalityKeyNames() { |
||||
return LowCardinalityKeyNames.values(); |
||||
} |
||||
|
||||
@Override |
||||
public KeyName[] getHighCardinalityKeyNames() { |
||||
return new KeyName[] {}; |
||||
} |
||||
|
||||
}; |
||||
|
||||
public enum LowCardinalityKeyNames implements KeyName { |
||||
|
||||
/** |
||||
* {@link Class#getSimpleName() Simple name} of the target type that owns the scheduled method. |
||||
*/ |
||||
TARGET_TYPE { |
||||
@Override |
||||
public String asString() { |
||||
return "target.type"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Name of the method that is executed for the scheduled task. |
||||
*/ |
||||
METHOD_NAME { |
||||
@Override |
||||
public String asString() { |
||||
return "method.name"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Name of the exception thrown during task execution, or {@value KeyValue#NONE_VALUE} if no exception was thrown. |
||||
*/ |
||||
EXCEPTION { |
||||
@Override |
||||
public String asString() { |
||||
return "exception"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Outcome of the scheduled task execution. |
||||
*/ |
||||
OUTCOME { |
||||
@Override |
||||
public String asString() { |
||||
return "outcome"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,296 @@
@@ -0,0 +1,296 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.annotation; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.concurrent.CountDownLatch; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import io.micrometer.observation.Observation; |
||||
import io.micrometer.observation.ObservationRegistry; |
||||
import io.micrometer.observation.tck.TestObservationRegistry; |
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert; |
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.observability.DefaultSignalListener; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition; |
||||
import org.springframework.beans.factory.support.RootBeanDefinition; |
||||
import org.springframework.context.support.StaticApplicationContext; |
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor; |
||||
import org.springframework.scheduling.config.ScheduledTask; |
||||
import org.springframework.scheduling.config.ScheduledTaskHolder; |
||||
import org.springframework.scheduling.config.ScheduledTaskObservationContext; |
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
|
||||
/** |
||||
* Observability tests for {@link ScheduledAnnotationBeanPostProcessor}. |
||||
* |
||||
* @author Brian Clozel |
||||
*/ |
||||
class ScheduledAnnotationBeanPostProcessorObservabilityTests { |
||||
|
||||
private final StaticApplicationContext context = new StaticApplicationContext(); |
||||
|
||||
private final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); |
||||
|
||||
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); |
||||
|
||||
@AfterEach |
||||
void closeContext() { |
||||
context.close(); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordSuccessObservationsForTasks() throws Exception { |
||||
registerScheduledBean(FixedDelayBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") |
||||
.hasLowCardinalityKeyValue("method.name", "fixedDelay") |
||||
.hasLowCardinalityKeyValue("target.type", "FixedDelayBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordFailureObservationsForTasksThrowing() throws Exception { |
||||
registerScheduledBean(FixedDelayErrorBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR") |
||||
.hasLowCardinalityKeyValue("method.name", "error") |
||||
.hasLowCardinalityKeyValue("target.type", "FixedDelayErrorBean") |
||||
.hasLowCardinalityKeyValue("exception", "IllegalStateException"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordSuccessObservationsForReactiveTasks() throws Exception { |
||||
registerScheduledBean(FixedDelayReactiveBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") |
||||
.hasLowCardinalityKeyValue("method.name", "fixedDelay") |
||||
.hasLowCardinalityKeyValue("target.type", "FixedDelayReactiveBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordFailureObservationsForReactiveTasksThrowing() throws Exception { |
||||
registerScheduledBean(FixedDelayReactiveErrorBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "ERROR") |
||||
.hasLowCardinalityKeyValue("method.name", "error") |
||||
.hasLowCardinalityKeyValue("target.type", "FixedDelayReactiveErrorBean") |
||||
.hasLowCardinalityKeyValue("exception", "IllegalStateException"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordCancelledObservationsForTasks() throws Exception { |
||||
registerScheduledBean(CancelledTaskBean.class); |
||||
ScheduledTask scheduledTask = getScheduledTask(); |
||||
this.taskExecutor.execute(scheduledTask.getTask().getRunnable()); |
||||
context.getBean(TaskTester.class).await(); |
||||
scheduledTask.cancel(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN") |
||||
.hasLowCardinalityKeyValue("method.name", "cancelled") |
||||
.hasLowCardinalityKeyValue("target.type", "CancelledTaskBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldRecordCancelledObservationsForReactiveTasks() throws Exception { |
||||
registerScheduledBean(CancelledReactiveTaskBean.class); |
||||
ScheduledTask scheduledTask = getScheduledTask(); |
||||
this.taskExecutor.execute(scheduledTask.getTask().getRunnable()); |
||||
context.getBean(TaskTester.class).await(); |
||||
scheduledTask.cancel(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN") |
||||
.hasLowCardinalityKeyValue("method.name", "cancelled") |
||||
.hasLowCardinalityKeyValue("target.type", "CancelledReactiveTaskBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldHaveCurrentObservationInScope() throws Exception { |
||||
registerScheduledBean(CurrentObservationBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") |
||||
.hasLowCardinalityKeyValue("method.name", "hasCurrentObservation") |
||||
.hasLowCardinalityKeyValue("target.type", "CurrentObservationBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldHaveCurrentObservationInReactiveScope() throws Exception { |
||||
registerScheduledBean(CurrentObservationReactiveBean.class); |
||||
runScheduledTaskAndAwait(); |
||||
assertThatTaskObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") |
||||
.hasLowCardinalityKeyValue("method.name", "hasCurrentObservation") |
||||
.hasLowCardinalityKeyValue("target.type", "CurrentObservationReactiveBean") |
||||
.hasLowCardinalityKeyValue("exception", "none"); |
||||
} |
||||
|
||||
|
||||
private void registerScheduledBean(Class<?> beanClass) { |
||||
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); |
||||
BeanDefinition targetDefinition = new RootBeanDefinition(beanClass); |
||||
targetDefinition.getPropertyValues().add("observationRegistry", this.observationRegistry); |
||||
context.registerBeanDefinition("postProcessor", processorDefinition); |
||||
context.registerBeanDefinition("target", targetDefinition); |
||||
context.registerBean("schedulingConfigurer", SchedulingConfigurer.class, () -> { |
||||
return new SchedulingConfigurer() { |
||||
@Override |
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { |
||||
taskRegistrar.setObservationRegistry(observationRegistry); |
||||
} |
||||
}; |
||||
}); |
||||
context.refresh(); |
||||
} |
||||
|
||||
private ScheduledTask getScheduledTask() { |
||||
ScheduledTaskHolder taskHolder = context.getBean("postProcessor", ScheduledTaskHolder.class); |
||||
return taskHolder.getScheduledTasks().iterator().next(); |
||||
} |
||||
|
||||
private void runScheduledTaskAndAwait() throws InterruptedException { |
||||
ScheduledTask scheduledTask = getScheduledTask(); |
||||
try { |
||||
scheduledTask.getTask().getRunnable().run(); |
||||
} |
||||
catch (Throwable exc) { |
||||
// ignore exceptions thrown by test tasks
|
||||
} |
||||
context.getBean(TaskTester.class).await(); |
||||
} |
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatTaskObservation() { |
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry) |
||||
.hasObservationWithNameEqualTo("tasks.scheduled.execution").that(); |
||||
} |
||||
|
||||
static abstract class TaskTester { |
||||
|
||||
ObservationRegistry observationRegistry; |
||||
|
||||
CountDownLatch latch = new CountDownLatch(1); |
||||
|
||||
public void setObservationRegistry(ObservationRegistry observationRegistry) { |
||||
this.observationRegistry = observationRegistry; |
||||
} |
||||
|
||||
public void await() throws InterruptedException { |
||||
this.latch.await(3, TimeUnit.SECONDS); |
||||
} |
||||
} |
||||
|
||||
static class FixedDelayBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public void fixedDelay() { |
||||
this.latch.countDown(); |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
static class FixedDelayErrorBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public void error() { |
||||
this.latch.countDown(); |
||||
throw new IllegalStateException("test error"); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class FixedDelayReactiveBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public Mono<Object> fixedDelay() { |
||||
return Mono.empty().doOnTerminate(() -> this.latch.countDown()); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class FixedDelayReactiveErrorBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public Mono<Object> error() { |
||||
return Mono.error(new IllegalStateException("test error")) |
||||
.doOnTerminate(() -> this.latch.countDown()); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class CancelledTaskBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public void cancelled() { |
||||
this.latch.countDown(); |
||||
try { |
||||
Thread.sleep(5000); |
||||
} |
||||
catch (InterruptedException exc) { |
||||
// ignore cancelled task
|
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
static class CancelledReactiveTaskBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public Flux<Long> cancelled() { |
||||
return Flux.interval(Duration.ZERO, Duration.ofSeconds(1)) |
||||
.doOnNext(el -> this.latch.countDown()); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class CurrentObservationBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public void hasCurrentObservation() { |
||||
assertThat(this.observationRegistry.getCurrentObservation()).isNotNull(); |
||||
assertThat(this.observationRegistry.getCurrentObservation().getContext()).isInstanceOf(ScheduledTaskObservationContext.class); |
||||
this.latch.countDown(); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class CurrentObservationReactiveBean extends TaskTester { |
||||
|
||||
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000) |
||||
public Mono<String> hasCurrentObservation() { |
||||
return Mono.just("test") |
||||
.tap(() -> new DefaultSignalListener<String>() { |
||||
@Override |
||||
public void doFirst() throws Throwable { |
||||
Observation observation = observationRegistry.getCurrentObservation(); |
||||
assertThat(observation).isNotNull(); |
||||
assertThat(observation.getContext()).isInstanceOf(ScheduledTaskObservationContext.class); |
||||
} |
||||
}) |
||||
.doOnTerminate(() -> this.latch.countDown()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
|
||||
/* |
||||
* Copyright 2002-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.config; |
||||
|
||||
|
||||
import java.lang.reflect.Method; |
||||
|
||||
import io.micrometer.common.KeyValue; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.aop.framework.ProxyFactory; |
||||
import org.springframework.aop.target.SingletonTargetSource; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link DefaultScheduledTaskObservationConvention}. |
||||
*/ |
||||
class DefaultScheduledTaskObservationConventionTests { |
||||
|
||||
private final Method taskMethod = ClassUtils.getMethod(BeanWithScheduledMethods.class, "process"); |
||||
|
||||
private final ScheduledTaskObservationConvention convention = new DefaultScheduledTaskObservationConvention(); |
||||
|
||||
@Test |
||||
void observationShouldHaveDefaultName() { |
||||
assertThat(convention.getName()).isEqualTo("tasks.scheduled.execution"); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveContextualName() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process"); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveContextualNameForProxiedClass() { |
||||
Object proxy = ProxyFactory.getProxy(new SingletonTargetSource(new BeanWithScheduledMethods())); |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(proxy, taskMethod); |
||||
assertThat(convention.getContextualName(context)).isEqualTo("task beanWithScheduledMethods.process"); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveTargetType() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("target.type", "BeanWithScheduledMethods")); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveMethodName() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("method.name", "process")); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveSuccessfulOutcome() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
context.setComplete(true); |
||||
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "SUCCESS"), |
||||
KeyValue.of("exception", "none")); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveErrorOutcome() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
context.setError(new IllegalStateException("test error")); |
||||
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "ERROR"), |
||||
KeyValue.of("exception", "IllegalStateException")); |
||||
} |
||||
|
||||
@Test |
||||
void observationShouldHaveUnknownOutcome() { |
||||
ScheduledTaskObservationContext context = new ScheduledTaskObservationContext(new BeanWithScheduledMethods(), taskMethod); |
||||
assertThat(convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("outcome", "UNKNOWN"), |
||||
KeyValue.of("exception", "none")); |
||||
} |
||||
|
||||
|
||||
static class BeanWithScheduledMethods implements TaskProcessor { |
||||
|
||||
public void process() { |
||||
|
||||
} |
||||
} |
||||
|
||||
interface TaskProcessor { |
||||
|
||||
void process(); |
||||
|
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue