Trigger cancellation on context close for non-managed objects only

Specifically for prototype/scoped beans and FactoryBean-exposed objects.

Closes gh-33009
This commit is contained in:
Juergen Hoeller
2024-06-12 13:31:00 +02:00
parent 261dac87cc
commit 0ff200b2f1
3 changed files with 195 additions and 32 deletions

View File

@@ -21,18 +21,24 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.testfixture.EnabledForTestGroups;
@@ -59,12 +65,20 @@ class EnableSchedulingTests {
private AnnotationConfigApplicationContext ctx;
private static final AtomicBoolean shutdownFailure = new AtomicBoolean();
@BeforeEach
void reset() {
shutdownFailure.set(false);
}
@AfterEach
void tearDown() {
if (ctx != null) {
ctx.close();
}
assertThat(shutdownFailure).isFalse();
}
@@ -75,7 +89,7 @@ class EnableSchedulingTests {
@ParameterizedTest
@ValueSource(classes = {FixedRateTaskConfig.class, FixedRateTaskConfigSubclass.class})
@EnabledForTestGroups(LONG_RUNNING)
public void withFixedRateTask(Class<?> configClass) throws InterruptedException {
void withFixedRateTask(Class<?> configClass) throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(configClass);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2);
@@ -92,7 +106,7 @@ class EnableSchedulingTests {
@ValueSource(classes = {ExplicitSchedulerConfig.class, ExplicitSchedulerConfigSubclass.class})
@Timeout(2) // should actually complete within 1s
@EnabledForTestGroups(LONG_RUNNING)
public void withExplicitScheduler(Class<?> configClass) throws InterruptedException {
void withExplicitScheduler(Class<?> configClass) throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(configClass);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
@@ -147,7 +161,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withExplicitScheduledTaskRegistrar() throws InterruptedException {
void withExplicitScheduledTaskRegistrar() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
@@ -158,7 +172,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withQualifiedScheduler() throws InterruptedException {
void withQualifiedScheduler() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfig.class);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
@@ -169,7 +183,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withQualifiedSchedulerAndPlaceholder() throws InterruptedException {
void withQualifiedSchedulerAndPlaceholder() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithPlaceholder.class);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
@@ -181,7 +195,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException {
void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class);
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
@@ -204,7 +218,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException {
void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(
SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class);
@@ -214,7 +228,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException {
void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(
SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class);
@@ -224,7 +238,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withTaskAddedVia_configureTasks() throws InterruptedException {
void withTaskAddedVia_configureTasks() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class);
Thread.sleep(110);
@@ -233,7 +247,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withInitiallyDelayedFixedRateTask() throws InterruptedException {
void withInitiallyDelayedFixedRateTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig_withInitialDelay.class);
Thread.sleep(1950);
@@ -246,7 +260,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withInitiallyDelayedFixedDelayTask() throws InterruptedException {
void withInitiallyDelayedFixedDelayTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withInitialDelay.class);
Thread.sleep(1950);
@@ -259,7 +273,35 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withOneTimeTask() throws InterruptedException {
void withPrototypeContainedFixedDelayTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withPrototypeBean.class);
ctx.getBean(PrototypeBeanWithScheduled.class);
Thread.sleep(1950);
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
// The @Scheduled method should have been called several times
// but not more times than the delay allows.
assertThat(counter.get()).isBetween(1, 5);
}
@Test
@EnabledForTestGroups(LONG_RUNNING)
void withPrototypeFactoryContainedFixedDelayTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withFactoryBean.class);
ctx.getBean(PrototypeBeanWithScheduled.class);
Thread.sleep(1950);
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
// The @Scheduled method should have been called several times
// but not more times than the delay allows.
assertThat(counter.get()).isBetween(1, 5);
}
@Test
@EnabledForTestGroups(LONG_RUNNING)
void withOneTimeTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(OneTimeTaskConfig.class);
Thread.sleep(110);
@@ -271,7 +313,7 @@ class EnableSchedulingTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withTriggerTask() throws InterruptedException {
void withTriggerTask() throws InterruptedException {
ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class);
Thread.sleep(110);
@@ -677,6 +719,9 @@ class EnableSchedulingTests {
@EnableScheduling
static class FixedRateTaskConfig_withInitialDelay {
@Autowired
ScheduledAnnotationBeanPostProcessor bpp;
@Bean
public AtomicInteger counter() {
return new AtomicInteger();
@@ -687,6 +732,13 @@ class EnableSchedulingTests {
counter().incrementAndGet();
Thread.sleep(100);
}
@PreDestroy
public void validateLateCancellation() {
if (this.bpp.getScheduledTasks().isEmpty()) {
shutdownFailure.set(true);
}
}
}
@@ -694,6 +746,9 @@ class EnableSchedulingTests {
@EnableScheduling
static class FixedDelayTaskConfig_withInitialDelay {
@Autowired
ScheduledAnnotationBeanPostProcessor bpp;
@Bean
public AtomicInteger counter() {
return new AtomicInteger();
@@ -704,6 +759,101 @@ class EnableSchedulingTests {
counter().incrementAndGet();
Thread.sleep(100);
}
@PreDestroy
public void validateLateCancellation() {
if (this.bpp.getScheduledTasks().isEmpty()) {
shutdownFailure.set(true);
}
}
}
@Configuration
@EnableScheduling
static class FixedDelayTaskConfig_withPrototypeBean {
@Autowired
ScheduledAnnotationBeanPostProcessor bpp;
@Bean
public AtomicInteger counter() {
return new AtomicInteger();
}
@Bean @Scope("prototype")
public PrototypeBeanWithScheduled prototypeBean() {
return new PrototypeBeanWithScheduled(counter());
}
@PreDestroy
public void validateEarlyCancellation() {
if (!this.bpp.getScheduledTasks().isEmpty()) {
shutdownFailure.set(true);
}
}
}
@Configuration
@EnableScheduling
static class FixedDelayTaskConfig_withFactoryBean {
@Autowired
ScheduledAnnotationBeanPostProcessor bpp;
@Bean
public AtomicInteger counter() {
return new AtomicInteger();
}
@Bean
public FactoryBeanForScheduled prototypeBean() {
return new FactoryBeanForScheduled(counter());
}
@PreDestroy
public void validateEarlyCancellation() {
if (!this.bpp.getScheduledTasks().isEmpty()) {
shutdownFailure.set(true);
}
}
}
static class PrototypeBeanWithScheduled {
private AtomicInteger counter;
public PrototypeBeanWithScheduled(AtomicInteger counter) {
this.counter = counter;
}
@Scheduled(initialDelay = 1000, fixedDelay = 100)
public void task() throws InterruptedException {
this.counter.incrementAndGet();
Thread.sleep(100);
}
}
static class FactoryBeanForScheduled implements FactoryBean<PrototypeBeanWithScheduled> {
private AtomicInteger counter;
public FactoryBeanForScheduled(AtomicInteger counter) {
this.counter = counter;
}
@Override
public PrototypeBeanWithScheduled getObject() {
return new PrototypeBeanWithScheduled(this.counter);
}
@Override
public Class<?> getObjectType() {
return PrototypeBeanWithScheduled.class;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -352,9 +352,6 @@ class ScheduledAnnotationBeanPostProcessorTests {
assertThat(condition).isTrue();
CronTrigger cronTrigger = (CronTrigger) trigger;
ZonedDateTime dateTime = ZonedDateTime.of(2013, 4, 15, 4, 0, 0, 0, ZoneId.of("GMT+10"));
// Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+10"));
// cal.clear();
// cal.set(2013, 3, 15, 4, 0); // 15-04-2013 4:00 GMT+10;
Instant lastScheduledExecution = dateTime.toInstant();
Instant lastActualExecution = dateTime.toInstant();
dateTime = dateTime.plusMinutes(30);
@@ -1026,6 +1023,7 @@ class ScheduledAnnotationBeanPostProcessorTests {
}
}
static class PropertyPlaceholderWithFixedDelayInSeconds {
@Scheduled(fixedDelayString = "${fixedDelay}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS)
@@ -1041,6 +1039,7 @@ class ScheduledAnnotationBeanPostProcessorTests {
}
}
static class PropertyPlaceholderWithFixedRateInSeconds {
@Scheduled(fixedRateString = "${fixedRate}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS)
@@ -1071,9 +1070,11 @@ class ScheduledAnnotationBeanPostProcessorTests {
}
}
@Retention(RetentionPolicy.RUNTIME)
@ConvertWith(NameToClass.Converter.class)
private @interface NameToClass {
class Converter implements ArgumentConverter {
@Override
public Class<?> convert(Object beanClassName, ParameterContext context) throws ArgumentConversionException {