Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduler - support static scheduled methods #24248

Merged
merged 1 commit into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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