Skip to content

Commit 2f533b5

Browse files
authored
[4.x] Scheduling fixes (#10837)
* Add isEnabled to scheduling tasks * Vetoed CDI beans shouldn't be scheduled #9828 * Fix intermittent failure in executedEvery2Sec #10682 Signed-off-by: Daniel Kec <[email protected]>
1 parent 5aa25ea commit 2f533b5

File tree

8 files changed

+121
-18
lines changed

8 files changed

+121
-18
lines changed

microprofile/scheduling/src/main/java/io/helidon/microprofile/scheduling/SchedulingCdiExtension.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ void invoke(@Observes @Priority(PLATFORM_AFTER + 4000) @Initialized(ApplicationS
112112
for (AnnotatedMethod<?> am : methods) {
113113
Class<?> aClass = am.getDeclaringType().getJavaClass();
114114
Bean<?> bean = beans.get(am);
115+
if (bean == null) {
116+
continue;
117+
}
115118
Object beanInstance = lookup(bean, beanManager);
116119
Method method = am.getJavaMember();
117120

microprofile/scheduling/src/test/java/io/helidon/microprofile/scheduling/SchedulingTest.java

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package io.helidon.microprofile.scheduling;
1818

1919
import java.time.Duration;
20+
import java.time.Instant;
2021
import java.time.LocalTime;
22+
import java.util.List;
2123
import java.util.concurrent.CompletableFuture;
24+
import java.util.concurrent.CopyOnWriteArrayList;
2225
import java.util.concurrent.CountDownLatch;
2326
import java.util.concurrent.ExecutionException;
2427
import java.util.concurrent.TimeUnit;
@@ -44,11 +47,14 @@
4447
import static org.hamcrest.Matchers.equalTo;
4548
import static org.hamcrest.Matchers.greaterThan;
4649
import static org.hamcrest.Matchers.lessThan;
50+
import static org.junit.jupiter.api.Assertions.assertFalse;
4751

4852
@HelidonTest
4953
@DisableDiscovery
5054
@AddBean(SchedulingTest.ScheduledBean.class)
55+
@AddBean(SchedulingTest.ScheduledVetoMeBean.class)
5156
@AddExtension(SchedulingCdiExtension.class)
57+
@AddExtension(TestVetoingCdiExtension.class)
5258
@Configuration(configSources = "test.properties")
5359
public class SchedulingTest {
5460

@@ -120,7 +126,13 @@ void expressionPlaceHolder() throws InterruptedException {
120126
void executedEvery2Sec() throws InterruptedException {
121127
assertThat("Scheduled method expected to be invoked at least twice",
122128
scheduledBean.getCountDownLatch().await(5, TimeUnit.SECONDS));
123-
assertDuration(TWO_SEC_MILLIS, scheduledBean.getDuration(), 2000);
129+
assertDuration(TWO_SEC_MILLIS, scheduledBean.getDurations(), TWO_SEC_MILLIS);
130+
}
131+
132+
@Test
133+
void vetoedShouldNotBeInvoked() throws InterruptedException {
134+
Thread.sleep(3000);
135+
assertFalse(ScheduledVetoMeBean.triggered);
124136
}
125137

126138
@Test
@@ -166,8 +178,9 @@ interval, is(Duration.ofSeconds(2)))
166178
);
167179
}
168180

169-
private void assertDuration(long expectedDuration, long duration, long allowedDiscrepancy) {
170-
String durationString = "Expected duration is 2 sec, but was " + ((float) duration / 1000) + "sec";
181+
private void assertDuration(long expectedDuration, List<Duration> durations, long allowedDiscrepancy) {
182+
var duration = durations.getFirst().toMillis();
183+
String durationString = "Expected duration is " + expectedDuration + " mls, but was " + duration + " mls";
171184
assertThat(durationString, duration, greaterThan(expectedDuration - allowedDiscrepancy));
172185
assertThat(durationString, duration, lessThan(expectedDuration + allowedDiscrepancy));
173186
}
@@ -179,6 +192,7 @@ public static class ScheduledBean {
179192

180193
final CountDownLatch countDownLatch = new CountDownLatch(2);
181194

195+
private final List<Duration> durations = new CopyOnWriteArrayList<>();
182196

183197
volatile long duration = 0;
184198
volatile long stamp = 0;
@@ -187,16 +201,33 @@ public CountDownLatch getCountDownLatch() {
187201
return countDownLatch;
188202
}
189203

190-
public long getDuration() {
191-
return duration;
204+
public List<Duration> getDurations() {
205+
return durations;
192206
}
193207

194208
@Scheduling.Cron("0/2 * * * * ? *")
195209
public void test2sec() {
196-
duration = System.currentTimeMillis() - stamp;
210+
if(stamp != 0) {
211+
duration = System.currentTimeMillis() - stamp;
212+
durations.add(Duration.ofMillis(duration));
213+
}
197214
stamp = System.currentTimeMillis();
215+
LOGGER.log(System.Logger.Level.DEBUG, () -> Thread.currentThread().getName() + " Executed at " + Instant.ofEpochMilli(stamp) + "(" + stamp);
198216
countDownLatch.countDown();
199-
LOGGER.log(System.Logger.Level.DEBUG, () -> "Executed at " + LocalTime.now().toString());
217+
}
218+
}
219+
220+
@ApplicationScoped
221+
public static class ScheduledVetoMeBean {
222+
223+
private static final System.Logger LOGGER = System.getLogger(ScheduledVetoMeBean.class.getName());
224+
225+
static volatile boolean triggered = false;
226+
227+
@Scheduling.Cron("0/1 * * * * ? *")
228+
public void test1sec() {
229+
triggered = true;
230+
LOGGER.log(System.Logger.Level.DEBUG, () -> "Executed at " + LocalTime.now());
200231
}
201232
}
202233
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.microprofile.scheduling;
18+
19+
import jakarta.enterprise.event.Observes;
20+
import jakarta.enterprise.inject.spi.AnnotatedType;
21+
import jakarta.enterprise.inject.spi.Extension;
22+
import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
23+
24+
/**
25+
* CDI Extension that vetoes any CDI bean whose class name contains "VetoMe".
26+
*/
27+
public class TestVetoingCdiExtension implements Extension {
28+
29+
void processAnnotatedType(@Observes ProcessAnnotatedType<?> pat) {
30+
AnnotatedType<?> annotatedType = pat.getAnnotatedType();
31+
Class<?> clazz = annotatedType.getJavaClass();
32+
33+
if (clazz.getSimpleName().contains("VetoMe")) {
34+
pat.veto();
35+
}
36+
}
37+
}

scheduling/src/main/java/io/helidon/scheduling/CronTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ public void close() {
142142
}
143143
}
144144

145+
@Override
146+
public boolean enabled() {
147+
return config.enabled();
148+
}
149+
145150
@Override
146151
public String id() {
147152
return taskId;

scheduling/src/main/java/io/helidon/scheduling/FixedRateTask.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public void close() {
9393
config.taskManager().remove(this);
9494
}
9595

96+
@Override
97+
public boolean enabled() {
98+
return config.enabled();
99+
}
100+
96101
@Override
97102
public String id() {
98103
return taskId;

scheduling/src/main/java/io/helidon/scheduling/Task.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ public interface Task {
4242
default void close() {
4343
}
4444

45+
/**
46+
* If task is enabled or disabled, scheduling tasks are enabled by default
47+
* and can be disabled programmatically or via configuration.
48+
*
49+
* @return true if enabled
50+
*/
51+
default boolean enabled() {
52+
return true;
53+
}
54+
4555
/**
4656
* ID used to identify this task.
4757
* It should be unique, as it is used to identify a single task, for example to cancel it.

scheduling/src/test/java/io/helidon/scheduling/CronSchedulingTest.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@
3636

3737
import static org.hamcrest.CoreMatchers.hasItem;
3838
import static org.hamcrest.MatcherAssert.assertThat;
39+
import static org.hamcrest.Matchers.is;
3940
import static org.hamcrest.collection.IsEmptyCollection.empty;
4041

4142
@SuppressWarnings("removal")
4243
@Testing.Test
4344
@Execution(ExecutionMode.CONCURRENT)
44-
public class CronSchedulingTest {
45+
class CronSchedulingTest {
4546

4647
static final long ERROR_MARGIN_MILLIS = 500;
4748

@@ -309,24 +310,25 @@ void cronMissingTask() {
309310
void cronDisabledProgrammatically() throws InterruptedException {
310311
ScheduledExecutorService executorService = ScheduledThreadPoolSupplier.create().get();
311312
IntervalMeter meter = new IntervalMeter();
312-
Cron cron = null;
313+
Cron task = null;
313314

314315
try {
315-
cron = Cron.builder()
316+
task = Cron.builder()
316317
.id("disabledCronTest")
317318
.executor(executorService)
318319
.expression("* * * * * ? *") // Every second
319320
.enabled(false) // Disabled
320321
.task(cronInvocation -> meter.start().end())
321322
.build();
322323

323-
// Wait and verify task never executed
324+
// Wait and verify the task never executed
324325
Thread.sleep(3000);
325326
assertThat(meter.size(), Matchers.equalTo(0));
326-
assertThat(taskManager.tasks(), hasItem(cron));
327+
assertThat(taskManager.tasks(), hasItem(task));
328+
assertThat(task.enabled(), is(false));
327329
} finally {
328-
if (cron != null) {
329-
cron.close();
330+
if (task != null) {
331+
task.close();
330332
}
331333
executorService.shutdownNow();
332334
}

scheduling/src/test/java/io/helidon/scheduling/FixedRateSchedulingTest.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,24 @@
3232
import org.junit.jupiter.api.parallel.Execution;
3333
import org.junit.jupiter.api.parallel.ExecutionMode;
3434

35+
import static org.hamcrest.CoreMatchers.hasItem;
3536
import static org.hamcrest.MatcherAssert.assertThat;
37+
import static org.hamcrest.Matchers.equalTo;
3638
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
3739
import static org.junit.jupiter.api.Assertions.assertTrue;
3840

3941
@Testing.Test
4042
@Execution(ExecutionMode.CONCURRENT)
41-
public class FixedRateSchedulingTest {
43+
class FixedRateSchedulingTest {
4244

4345
static final long ERROR_MARGIN_MILLIS = 500;
4446

47+
private final TaskManager taskManager;
48+
49+
FixedRateSchedulingTest(TaskManager taskManager) {
50+
this.taskManager = taskManager;
51+
}
52+
4553
@SuppressWarnings("removal")
4654
@Test
4755
void fixedRateDelayDeprecated() {
@@ -314,16 +322,18 @@ void fixedRateDisabled() throws InterruptedException {
314322
IntervalMeter meter = new IntervalMeter();
315323

316324
try {
317-
FixedRate.builder()
325+
var task = FixedRate.builder()
318326
.executor(executorService)
319327
.interval(Duration.ofSeconds(1))
320328
.enabled(false) // Disabled
321329
.task(inv -> meter.start().end())
322330
.build();
323331

324-
// Wait and verify task never executed
332+
// Wait and verify the task never executed
325333
Thread.sleep(3000);
326-
assertThat(meter.size(), Matchers.equalTo(0));
334+
assertThat(meter.size(), equalTo(0));
335+
assertThat(taskManager.tasks(), hasItem(task));
336+
assertThat(task.enabled(), equalTo(false));
327337
} finally {
328338
executorService.shutdownNow();
329339
}

0 commit comments

Comments
 (0)