Skip to content

Commit f6d8b7c

Browse files
authored
4.x: Support for tracking scheduled tasks. (helidon-io#10289)
* Support for tracking scheduled tasks. * Adding public API for TaskManager. Update to declarative generated code to inject the TaskManager and to configure it on task builders. Update to documentation * - Allow users to override ID using configuration - use identity hash code as the default ID to have a unique task id if not implemented by task
1 parent 1b3f7aa commit f6d8b7c

15 files changed

Lines changed: 313 additions & 25 deletions

File tree

declarative/codegen/src/main/java/io/helidon/declarative/codegen/scheduling/SchedulingExtension.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import static io.helidon.declarative.codegen.scheduling.SchedulingTypes.FIXED_RATE;
5454
import static io.helidon.declarative.codegen.scheduling.SchedulingTypes.FIXED_RATE_ANNOTATION;
5555
import static io.helidon.declarative.codegen.scheduling.SchedulingTypes.FIXED_RATE_INVOCATION;
56+
import static io.helidon.declarative.codegen.scheduling.SchedulingTypes.TASK_MANAGER;
5657
import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_SINGLETON;
5758

5859
class SchedulingExtension implements RegistryCodegenExtension {
@@ -162,7 +163,8 @@ private void addPostConstruct(ClassModel.Builder classModel,
162163
.name("postConstruct")
163164
.accessModifier(AccessModifier.PACKAGE_PRIVATE);
164165

165-
postConstruct.addContentLine("var config = configSupplier.get();")
166+
postConstruct.addContentLine("var taskManager = taskManagerSupplier.get();")
167+
.addContentLine("var config = configSupplier.get();")
166168
.addContent("var classConfig = config.get(\"")
167169
.addContent(serviceType.fqName())
168170
.addContentLine("\");")
@@ -203,6 +205,11 @@ private void addStartScheduled(Method.Builder postConstruct, Scheduled scheduled
203205
}
204206
postConstruct.increaseContentPadding()
205207
.increaseContentPadding()
208+
.addContentLine(".taskManager(taskManager)")
209+
// ID must be configured before reading config, as config may modify it
210+
.addContent(".id(\"")
211+
.addContent(scheduled.id())
212+
.addContentLine("\")")
206213
.addContent(".config(")
207214
.addContent(configVariable)
208215
.addContent(".get(\"")
@@ -233,12 +240,20 @@ private void addConstructorAndFields(ClassModel.Builder classModel, TypeName ser
233240
TypeName serviceSupplierType = TypeName.builder(TypeNames.SUPPLIER)
234241
.addTypeArgument(serviceType)
235242
.build();
243+
TypeName taskManagerSupplierType = TypeName.builder(TypeNames.SUPPLIER)
244+
.addTypeArgument(TASK_MANAGER)
245+
.build();
236246

237247
classModel.addField(service -> service
238248
.accessModifier(AccessModifier.PRIVATE)
239249
.isFinal(true)
240250
.type(serviceSupplierType)
241251
.name("serviceSupplier"))
252+
.addField(taskManagerSupplier -> taskManagerSupplier
253+
.accessModifier(AccessModifier.PRIVATE)
254+
.isFinal(true)
255+
.name("taskManagerSupplier")
256+
.type(taskManagerSupplierType))
242257
.addField(configSupplier -> configSupplier
243258
.accessModifier(AccessModifier.PRIVATE)
244259
.isFinal(true)
@@ -260,16 +275,21 @@ private void addConstructorAndFields(ClassModel.Builder classModel, TypeName ser
260275
.addParameter(configSupplier -> configSupplier
261276
.name("configSupplier")
262277
.type(configSupplierType)
263-
.name("configSupplier")
278+
)
279+
.addParameter(taskManagerSupplier -> taskManagerSupplier
280+
.name("taskManagerSupplier")
281+
.type(taskManagerSupplierType)
264282
)
265283
.addParameter(service -> service
266284
.type(serviceSupplierType)
267285
.name("serviceSupplier"))
268286
.addContentLine("this.configSupplier = configSupplier;")
287+
.addContentLine("this.taskManagerSupplier = taskManagerSupplier;")
269288
.addContent("this.serviceSupplier = serviceSupplier;"));
270289
}
271290

272291
private void processCron(List<Scheduled> allScheduled,
292+
TypeName enclosingType,
273293
TypedElementInfo element) {
274294

275295
// there can be a method argument, but it must be of correct type
@@ -286,6 +306,7 @@ private void processCron(List<Scheduled> allScheduled,
286306
// add for processing
287307
allScheduled.add(new Cron(element.elementName(),
288308
hasInvocationArgument,
309+
toId(enclosingType, element),
289310
expression,
290311
concurrent,
291312
configKey));
@@ -311,12 +332,32 @@ private void processFixedRate(List<Scheduled> allScheduled,
311332
// add for processing
312333
allScheduled.add(new FixedRate(element.elementName(),
313334
hasInvocationArgument,
335+
toId(enclosingType, element),
314336
rate,
315337
delayBy,
316338
delayType,
317339
configKey));
318340
}
319341

342+
private String toId(TypeName enclosingType, TypedElementInfo element) {
343+
StringBuilder sb = new StringBuilder();
344+
sb.append(enclosingType.fqName())
345+
.append(".")
346+
.append(element.elementName())
347+
.append("(");
348+
349+
if (!element.parameterArguments().isEmpty()) {
350+
// we can use just the class name, as there are only two options for declarative
351+
// FixedRateInvocation or CronInvocation, package not needed
352+
sb.append(element.parameterArguments().getFirst()
353+
.typeName()
354+
.className());
355+
}
356+
357+
sb.append(")");
358+
return sb.toString();
359+
}
360+
320361
private boolean checkAndHasArgument(TypedElementInfo element, TypeName annotationType, TypeName invocationArgumentType) {
321362
List<TypedElementInfo> typedElementInfos = element.parameterArguments();
322363
if (typedElementInfos.size() == 1
@@ -367,7 +408,7 @@ private void addCron(RegistryRoundContext roundContext,
367408
for (TypedElementInfo element : elements) {
368409
TypeName enclosingType = enclosingType(element);
369410
var allScheduled = scheduledByType.computeIfAbsent(enclosingType, k -> new ArrayList<>());
370-
processCron(allScheduled, element);
411+
processCron(allScheduled, enclosingType, element);
371412
}
372413

373414
}
@@ -380,10 +421,13 @@ private interface Scheduled {
380421
void createScheduledContent(ContentBuilder<?> content);
381422

382423
Optional<String> configKey();
424+
425+
String id();
383426
}
384427

385428
private record Cron(String methodName,
386429
boolean hasParameter,
430+
String id,
387431
String expression,
388432
boolean concurrent,
389433
Optional<String> configKey)
@@ -410,6 +454,7 @@ public void createScheduledContent(ContentBuilder<?> content) {
410454

411455
private record FixedRate(String methodName,
412456
boolean hasParameter,
457+
String id,
413458
String rate,
414459
String delayBy,
415460
String delayType,

declarative/codegen/src/main/java/io/helidon/declarative/codegen/scheduling/SchedulingTypes.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
final class SchedulingTypes {
2222
static final TypeName TASK = TypeName.create("io.helidon.scheduling.Task");
23+
static final TypeName TASK_MANAGER = TypeName.create("io.helidon.scheduling.TaskManager");
2324
static final TypeName FIXED_RATE = TypeName.create("io.helidon.scheduling.FixedRate");
2425
static final TypeName FIXED_RATE_ANNOTATION = TypeName.create("io.helidon.scheduling.Scheduling.FixedRate");
2526
static final TypeName FIXED_RATE_INVOCATION = TypeName.create("io.helidon.scheduling.FixedRateInvocation");

declarative/tests/codegen/src/test/java/io/helidon/declarative/codegen/scheduling/DeclarativeCodegenSchedulingTypesTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.helidon.scheduling.FixedRateInvocation;
3131
import io.helidon.scheduling.Scheduling;
3232
import io.helidon.scheduling.Task;
33+
import io.helidon.scheduling.TaskManager;
3334

3435
import org.hamcrest.collection.IsEmptyCollection;
3536
import org.junit.jupiter.api.Test;
@@ -64,6 +65,7 @@ void testTypes() {
6465
}
6566

6667
checkField(toCheck, checked, fields, "TASK", Task.class);
68+
checkField(toCheck, checked, fields, "TASK_MANAGER", TaskManager.class);
6769
checkField(toCheck, checked, fields, "FIXED_RATE", FixedRate.class);
6870
checkField(toCheck, checked, fields, "FIXED_RATE_ANNOTATION", Scheduling.FixedRate.class);
6971
checkField(toCheck, checked, fields, "FIXED_RATE_INVOCATION", FixedRateInvocation.class);

docs/src/main/asciidoc/se/scheduling.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ include::{rootdir}/includes/se.adoc[]
3434
- <<Configuration, Configuration>>
3535
- <<Cron, Cron>>
3636
- <<Fixed rate, Fixed Rate>>
37+
- <<Task Management, Task Management>>
3738
- <<Examples, Examples>>
3839
- <<Reference, Reference>>
3940
@@ -105,10 +106,17 @@ Scheduling is configurable with xref:../se/config/introduction.adoc[Helidon Conf
105106
include::{sourcedir}/se/SchedulingSnippets.java[tag=snippet_4, indent=0]
106107
----
107108
109+
== Task Management
110+
111+
A `io.helidon.scheduling.TaskManager` can be used to manage tasks that are started within Helidon.
112+
When using imperative programming model, you can either provide a custom implementation of this interface to task builder (method `taskManager`), or you can use the "default" one that can be obtained by invoking `io.helidon.service.registry.Services.get(TaskManager.class)`.
113+
When using the default `TaskManager` from `io.helidon.service.registry.Services`, there is no need to explicitly register it with the task builders.
114+
115+
When using declarative programming model, the `TaskManager` can be injected. It is a `Singleton` service that will be used by all scheduled tasks in the current application.
108116
109117
== Examples
110118
111-
=== Fixed rate
119+
=== Fixed Rate Example
112120
For simple fixed rate invocation use .
113121
114122
[source,java]

scheduling/pom.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
<artifactId>helidon-config-testing</artifactId>
6363
<scope>test</scope>
6464
</dependency>
65+
<dependency>
66+
<groupId>io.helidon.testing</groupId>
67+
<artifactId>helidon-testing-junit5</artifactId>
68+
<scope>test</scope>
69+
</dependency>
6570
</dependencies>
6671

6772
<build>
@@ -99,6 +104,11 @@
99104
<artifactId>helidon-builder-codegen</artifactId>
100105
<version>${helidon.version}</version>
101106
</path>
107+
<path>
108+
<groupId>io.helidon.service</groupId>
109+
<artifactId>helidon-service-codegen</artifactId>
110+
<version>${helidon.version}</version>
111+
</path>
102112
<path>
103113
<groupId>io.helidon.codegen</groupId>
104114
<artifactId>helidon-codegen-helidon-copyright</artifactId>
@@ -119,14 +129,19 @@
119129
</dependency>
120130
<dependency>
121131
<groupId>io.helidon.common.features</groupId>
122-
<artifactId>helidon-common-features-processor</artifactId>
132+
<artifactId>helidon-common-features-codegen</artifactId>
123133
<version>${helidon.version}</version>
124134
</dependency>
125135
<dependency>
126136
<groupId>io.helidon.builder</groupId>
127137
<artifactId>helidon-builder-codegen</artifactId>
128138
<version>${helidon.version}</version>
129139
</dependency>
140+
<dependency>
141+
<groupId>io.helidon.service</groupId>
142+
<artifactId>helidon-service-codegen</artifactId>
143+
<version>${helidon.version}</version>
144+
</dependency>
130145
<dependency>
131146
<groupId>io.helidon.codegen</groupId>
132147
<artifactId>helidon-codegen-helidon-copyright</artifactId>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.ZonedDateTime;
2222
import java.util.Locale;
2323
import java.util.Optional;
24+
import java.util.UUID;
2425
import java.util.concurrent.ScheduledExecutorService;
2526
import java.util.concurrent.ScheduledFuture;
2627
import java.util.concurrent.TimeUnit;
@@ -47,6 +48,7 @@ class CronTask implements Cron {
4748
private final com.cronutils.model.Cron cron;
4849
private final ReentrantLock scheduleNextLock = new ReentrantLock();
4950
private final CronConfig config;
51+
private final String taskId;
5052

5153
private ZonedDateTime lastNext = null;
5254

@@ -58,12 +60,14 @@ class CronTask implements Cron {
5860
this.executorService = config.executor();
5961
this.concurrentExecution = config.concurrentExecution();
6062
this.actualTask = config.task();
63+
this.taskId = config.id().orElseGet(() -> UUID.randomUUID().toString());
6164

6265
CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(QUARTZ);
6366
CronParser parser = new CronParser(cronDefinition);
6467
cron = parser.parse(config.expression());
6568
executionTime = ExecutionTime.forCron(cron);
6669

70+
config.taskManager().register(this);
6771
scheduleNext();
6872
}
6973

@@ -124,12 +128,23 @@ public void close() {
124128
stopped = true;
125129
if (future != null) {
126130
future.cancel(false);
131+
config.taskManager().remove(this);
127132
}
128133
} finally {
129134
scheduleNextLock.unlock();
130135
}
131136
}
132137

138+
@Override
139+
public String id() {
140+
return taskId;
141+
}
142+
143+
@Override
144+
public String toString() {
145+
return description() + "(" + taskId + ")";
146+
}
147+
133148
private void scheduleNext() {
134149
try {
135150
scheduleNextLock.lock();

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ public void decorate(FixedRateConfig.BuilderBase<?, ?> target) {
2929

3030
// new values are set using the option decorators below, now we can just re-set the deprecated values
3131
target.initialDelay(target.delayBy().toMillis());
32-
target.delay(target.interval().map(Duration::toMillis).orElse(1000L));
32+
target.interval()
33+
.map(Duration::toMillis)
34+
.ifPresent(target::delay);
3335
target.timeUnit(TimeUnit.MILLISECONDS);
3436
}
3537

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.System.Logger.Level;
2020
import java.time.Duration;
21+
import java.util.UUID;
2122
import java.util.concurrent.ScheduledExecutorService;
2223
import java.util.concurrent.ScheduledFuture;
2324
import java.util.concurrent.TimeUnit;
@@ -34,15 +35,16 @@ class FixedRateTask implements FixedRate {
3435
private final ScheduledConsumer<FixedRateInvocation> actualTask;
3536
private final ScheduledFuture<?> future;
3637
private final FixedRateConfig config;
38+
private final String taskId;
3739

3840
FixedRateTask(FixedRateConfig config) {
3941
this.config = config;
4042

4143
this.initialDelay = config.delayBy();
4244
this.interval = config.interval();
4345
this.actualTask = config.task();
44-
4546
this.executorService = config.executor();
47+
this.taskId = config.id().orElseGet(() -> UUID.randomUUID().toString());
4648

4749
this.future = switch (config.delayType()) {
4850
case SINCE_PREVIOUS_START -> executorService.scheduleAtFixedRate(this::run,
@@ -54,6 +56,8 @@ class FixedRateTask implements FixedRate {
5456
interval.toMillis(),
5557
TimeUnit.MILLISECONDS);
5658
};
59+
60+
config.taskManager().register(this);
5761
}
5862

5963
@Override
@@ -78,6 +82,17 @@ public ScheduledExecutorService executor() {
7882
@Override
7983
public void close() {
8084
future.cancel(false);
85+
config.taskManager().remove(this);
86+
}
87+
88+
@Override
89+
public String id() {
90+
return taskId;
91+
}
92+
93+
@Override
94+
public String toString() {
95+
return description() + "(" + taskId + ")";
8196
}
8297

8398
void run() {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@
2222
* {@link io.helidon.scheduling.Scheduling Scheduled} task.
2323
*/
2424
public interface Task {
25-
2625
/**
27-
* Human readable description of the task invocation interval.
26+
* Human-readable description of the task invocation interval.
2827
*
2928
* @return interval description
3029
*/
@@ -42,4 +41,15 @@ public interface Task {
4241
*/
4342
default void close() {
4443
}
44+
45+
/**
46+
* ID used to identify this task.
47+
* It should be unique, as it is used to identify a single task, for example to cancel it.
48+
*
49+
* @return task ID
50+
*/
51+
default String id() {
52+
// we need a unique id in this VM, this is implemented by Helidon tasks
53+
return String.valueOf(System.identityHashCode(this));
54+
}
4555
}

0 commit comments

Comments
 (0)