Skip to content

Commit

Permalink
Merge pull request #24248 from mkouba/issue-24234
Browse files Browse the repository at this point in the history
Scheduler - support static scheduled methods
  • Loading branch information
mkouba authored Mar 11, 2022
2 parents bbbd145 + 136df14 commit 0ed10af
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 57 deletions.
21 changes: 11 additions & 10 deletions docs/src/main/asciidoc/scheduler-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,23 @@ NOTE: If you add the `quarkus-quartz` dependency to your project the lightweight

== Scheduled Methods

If you annotate a method with `@io.quarkus.scheduler.Scheduled` it is automatically scheduled for invocation.
In fact, such a method must be a non-private non-static method of a CDI bean.
As a consequence of being a method of a CDI bean a scheduled method can be annotated with interceptor bindings, such as `@javax.transaction.Transactional` and `@org.eclipse.microprofile.metrics.annotation.Counted`.
A method annotated with `@io.quarkus.scheduler.Scheduled` is automatically scheduled for invocation.
A scheduled method must not be abstract or private.
It may be either static or non-static.
A scheduled method can be annotated with interceptor bindings, such as `@javax.transaction.Transactional` and `@org.eclipse.microprofile.metrics.annotation.Counted`.

NOTE: If there is no CDI scope defined on the declaring class then `@Singleton` is used.
NOTE: If there is a bean class that has no scope and declares at least one non-static method annotated with `@Scheduled` then `@Singleton` is used.

Furthermore, the annotated method must return `void` and either declare no parameters or one parameter of type `io.quarkus.scheduler.ScheduledExecution`.

TIP: The annotation is repeatable so a single method could be scheduled multiple times.
TIP: A CDI event of type `io.quarkus.scheduler.SuccessfulExecution` is fired synchronously and asynchronously when an execution of a scheduled method is successful.
TIP: A CDI event of type `io.quarkus.scheduler.FailedExecution` is fired synchronously and asynchronously when an execution of a scheduled method throw an exception.

TIP: A CDI event of type `io.quarkus.scheduler.SuccessfulExecution` is fired synchronously and asynchronously when an execution of a scheduled method is successful. A CDI event of type `io.quarkus.scheduler.FailedExecution` is fired synchronously and asynchronously when an execution of a scheduled method throws an exception.


=== Triggers

A trigger is defined either by the `@Scheduled#cron()` or by the `@Scheduled#every()` attributes.
A trigger is defined either by the `@Scheduled#cron()` or by the `@Scheduled#every()` attribute.
If both are specified, the cron expression takes precedence.
If none is specified, the build fails with an `IllegalStateException`.

Expand Down Expand Up @@ -124,9 +125,9 @@ void myMethod() { }

=== Identity

By default, a unique id is generated for each scheduled method.
This id is used in log messages and during debugging.
Sometimes a possibility to specify an explicit id may come in handy.
By default, a unique identifier is generated for each scheduled method.
This identifier is used in log messages, during debugging and as a parameter of some `io.quarkus.scheduler.Scheduler` methods.
Therefore, a possibility to specify an explicit identifier may come in handy.

.Identity Example
[source,java]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;

import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BuiltinScope;
Expand Down Expand Up @@ -180,6 +182,25 @@ public Builder containsAnnotations(DotName... annotationNames) {
});
}

/**
* The class declares a method that matches the given predicate.
* <p>
* The final predicate is a short-circuiting logical AND of the previous predicate (if any) and this condition.
*
* @param predicate
* @return self
*/
public Builder anyMethodMatches(Predicate<MethodInfo> predicate) {
return and((clazz, annotations, index) -> {
for (MethodInfo method : clazz.methods()) {
if (predicate.test(method)) {
return true;
}
}
return false;
});
}

/**
* The class must directly or indirectly implement the given interface.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.quartz.test.staticmethod;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.scheduler.Scheduled;
import io.quarkus.test.QuarkusUnitTest;

public class ScheduledStaticMethodTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Jobs.class));

@Test
public void testSimpleScheduledJobs() throws InterruptedException {
assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS));
}

static class Jobs {

static final CountDownLatch LATCH = new CountDownLatch(1);

@Scheduled(every = "1s")
static void everySecond() {
LATCH.countDown();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public ScheduledBusinessMethodItem(BeanInfo bean, MethodInfo method, List<Annota
this.schedules = schedules;
}

/**
*
* @return the bean or {@code null} for a static method
*/
public BeanInfo getBean() {
return bean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -87,9 +85,6 @@
import io.quarkus.scheduler.runtime.devconsole.SchedulerDevConsoleRecorder;
import io.quarkus.scheduler.runtime.util.SchedulerUtils;

/**
* @author Martin Kouba
*/
public class SchedulerProcessor {

private static final Logger LOGGER = Logger.getLogger(SchedulerProcessor.class);
Expand All @@ -114,17 +109,40 @@ void beans(Capabilities capabilities, BuildProducer<AdditionalBeanBuildItem> add

@BuildStep
AutoAddScopeBuildItem autoAddScope() {
return AutoAddScopeBuildItem.builder().containsAnnotations(SCHEDULED_NAME, SCHEDULES_NAME)
// We add @Singleton to any bean class that has no scope annotation and declares at least one non-static method annotated with @Scheduled
return AutoAddScopeBuildItem.builder()
.anyMethodMatches(m -> !Modifier.isStatic(m.flags())
&& (m.hasAnnotation(SCHEDULED_NAME) || m.hasAnnotation(SCHEDULES_NAME)))
.defaultScope(BuiltinScope.SINGLETON)
.reason("Found scheduled business methods").build();
.reason("Found non-static scheduled business methods").build();
}

@BuildStep
void collectScheduledMethods(BeanArchiveIndexBuildItem beanArchives, BeanDiscoveryFinishedBuildItem beanDiscovery,
TransformedAnnotationsBuildItem transformedAnnotations,
BuildProducer<ScheduledBusinessMethodItem> scheduledBusinessMethods) {

// We need to collect all business methods annotated with @Scheduled first
// First collect static scheduled methods
List<AnnotationInstance> schedules = new ArrayList<>(beanArchives.getIndex().getAnnotations(SCHEDULED_NAME));
for (AnnotationInstance annotationInstance : beanArchives.getIndex().getAnnotations(SCHEDULES_NAME)) {
for (AnnotationInstance scheduledInstance : annotationInstance.value().asNestedArray()) {
// We need to set the target of the containing instance
schedules.add(AnnotationInstance.create(scheduledInstance.name(), annotationInstance.target(),
scheduledInstance.values()));
}
}
for (AnnotationInstance annotationInstance : schedules) {
if (annotationInstance.target().kind() != METHOD) {
continue;
}
MethodInfo method = annotationInstance.target().asMethod();
if (Modifier.isStatic(method.flags())) {
scheduledBusinessMethods.produce(new ScheduledBusinessMethodItem(null, method, schedules));
LOGGER.debugf("Found scheduled static method %s declared on %s", method, method.declaringClass().name());
}
}

// Then collect all business methods annotated with @Scheduled
for (BeanInfo bean : beanDiscovery.beanStream().classBeans()) {
collectScheduledMethods(beanArchives.getIndex(), transformedAnnotations, bean,
bean.getTarget().get().asClass(),
Expand All @@ -136,10 +154,14 @@ private void collectScheduledMethods(IndexView index, TransformedAnnotationsBuil
ClassInfo beanClass, BuildProducer<ScheduledBusinessMethodItem> scheduledBusinessMethods) {

for (MethodInfo method : beanClass.methods()) {
if (Modifier.isStatic(method.flags())) {
// Ignore static methods
continue;
}
List<AnnotationInstance> schedules = null;
AnnotationInstance scheduledAnnotation = transformedAnnotations.getAnnotation(method, SCHEDULED_NAME);
if (scheduledAnnotation != null) {
schedules = Collections.singletonList(scheduledAnnotation);
schedules = List.of(scheduledAnnotation);
} else {
AnnotationInstance schedulesAnnotation = transformedAnnotations.getAnnotation(method, SCHEDULES_NAME);
if (schedulesAnnotation != null) {
Expand Down Expand Up @@ -174,13 +196,16 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List<ScheduledBusi

for (ScheduledBusinessMethodItem scheduledMethod : scheduledMethods) {
MethodInfo method = scheduledMethod.getMethod();

if (Modifier.isPrivate(method.flags()) || Modifier.isStatic(method.flags())) {
errors.add(new IllegalStateException("@Scheduled method must be non-private and non-static: "
if (Modifier.isAbstract(method.flags())) {
errors.add(new IllegalStateException("@Scheduled method must not be abstract: "
+ method.declaringClass().name() + "#" + method.name() + "()"));
continue;
}
if (Modifier.isPrivate(method.flags())) {
errors.add(new IllegalStateException("@Scheduled method must not be private: "
+ method.declaringClass().name() + "#" + method.name() + "()"));
continue;
}

// Validate method params and return type
List<Type> params = method.parameters();
if (params.size() > 1
Expand Down Expand Up @@ -212,7 +237,7 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List<ScheduledBusi
@BuildStep
public List<UnremovableBeanBuildItem> unremovableBeans() {
// Beans annotated with @Scheduled should never be removed
return Arrays.asList(new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULED_NAME)),
return List.of(new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULED_NAME)),
new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULES_NAME)));
}

Expand Down Expand Up @@ -320,20 +345,22 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas

BeanInfo bean = scheduledMethod.getBean();
MethodInfo method = scheduledMethod.getMethod();
boolean isStatic = Modifier.isStatic(method.flags());
ClassInfo implClazz = isStatic ? method.declaringClass() : bean.getImplClazz();

String baseName;
if (bean.getImplClazz().enclosingClass() != null) {
baseName = DotNames.simpleName(bean.getImplClazz().enclosingClass()) + NESTED_SEPARATOR
+ DotNames.simpleName(bean.getImplClazz());
if (implClazz.enclosingClass() != null) {
baseName = DotNames.simpleName(implClazz.enclosingClass()) + NESTED_SEPARATOR
+ DotNames.simpleName(implClazz);
} else {
baseName = DotNames.simpleName(bean.getImplClazz().name());
baseName = DotNames.simpleName(implClazz.name());
}
StringBuilder sigBuilder = new StringBuilder();
sigBuilder.append(method.name()).append("_").append(method.returnType().name().toString());
for (Type i : method.parameters()) {
sigBuilder.append(i.name().toString());
}
String generatedName = DotNames.internalPackageNameWithTrailingSlash(bean.getImplClazz().name()) + baseName
String generatedName = DotNames.internalPackageNameWithTrailingSlash(implClazz.name()) + baseName
+ INVOKER_SUFFIX + "_" + method.name() + "_"
+ HashUtil.sha1(sigBuilder.toString());

Expand All @@ -344,33 +371,47 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas
// The descriptor is: void invokeBean(Object execution)
MethodCreator invoke = invokerCreator.getMethodCreator("invokeBean", void.class, Object.class)
.addException(Exception.class);
// InjectableBean<Foo: bean = Arc.container().bean("1");
// InstanceHandle<Foo> handle = Arc.container().instance(bean);
// handle.get().ping();
ResultHandle containerHandle = invoke
.invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
ResultHandle beanHandle = invoke.invokeInterfaceMethod(
MethodDescriptor.ofMethod(ArcContainer.class, "bean", InjectableBean.class, String.class),
containerHandle, invoke.load(bean.getIdentifier()));
ResultHandle instanceHandle = invoke.invokeInterfaceMethod(
MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, InjectableBean.class),
containerHandle, beanHandle);
ResultHandle beanInstanceHandle = invoke
.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle);
if (method.parameters().isEmpty()) {
invoke.invokeVirtualMethod(
MethodDescriptor.ofMethod(bean.getImplClazz().name().toString(), method.name(), void.class),
beanInstanceHandle);

if (isStatic) {
if (method.parameters().isEmpty()) {
invoke.invokeStaticMethod(
MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class));
} else {
invoke.invokeStaticMethod(
MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class,
ScheduledExecution.class),
invoke.getMethodParam(0));
}
} else {
invoke.invokeVirtualMethod(
MethodDescriptor.ofMethod(bean.getImplClazz().name().toString(), method.name(), void.class,
ScheduledExecution.class),
beanInstanceHandle, invoke.getMethodParam(0));
}
// handle.destroy() - destroy dependent instance afterwards
if (BuiltinScope.DEPENDENT.is(bean.getScope())) {
invoke.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "destroy", void.class),
instanceHandle);
// InjectableBean<Foo: bean = Arc.container().bean("1");
// InstanceHandle<Foo> handle = Arc.container().instance(bean);
// handle.get().ping();
ResultHandle containerHandle = invoke
.invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class));
ResultHandle beanHandle = invoke.invokeInterfaceMethod(
MethodDescriptor.ofMethod(ArcContainer.class, "bean", InjectableBean.class, String.class),
containerHandle, invoke.load(bean.getIdentifier()));
ResultHandle instanceHandle = invoke.invokeInterfaceMethod(
MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, InjectableBean.class),
containerHandle, beanHandle);
ResultHandle beanInstanceHandle = invoke
.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class),
instanceHandle);
if (method.parameters().isEmpty()) {
invoke.invokeVirtualMethod(
MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class),
beanInstanceHandle);
} else {
invoke.invokeVirtualMethod(
MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class,
ScheduledExecution.class),
beanInstanceHandle, invoke.getMethodParam(0));
}
// handle.destroy() - destroy dependent instance afterwards
if (BuiltinScope.DEPENDENT.is(bean.getScope())) {
invoke.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "destroy", void.class),
instanceHandle);
}
}
invoke.returnValue(null);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.scheduler.test.staticmethod;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.scheduler.Scheduled;
import io.quarkus.test.QuarkusUnitTest;

public class ScheduledStaticMethodTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Jobs.class));

@Test
public void testSimpleScheduledJobs() throws InterruptedException {
assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS));
}

static class Jobs {

static final CountDownLatch LATCH = new CountDownLatch(1);

@Scheduled(every = "1s")
static void everySecond() {
LATCH.countDown();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import io.quarkus.scheduler.Scheduled.Schedules;

/**
* Marks a business method to be automatically scheduled and invoked by the container.
* Identifies a method of a bean class that is automatically scheduled and invoked by the container.
* <p>
* The target business method must be non-private and non-static.
* A scheduled method is a non-abstract non-private method of a bean class. It may be either static or non-static.
* <p>
* The schedule is defined either by {@link #cron()} or by {@link #every()} attribute. If both are specified, the cron
* expression takes precedence.
Expand Down

0 comments on commit 0ed10af

Please sign in to comment.