diff --git a/cdi/shedlock-cdi-vintage/pom.xml b/cdi/shedlock-cdi-vintage/pom.xml new file mode 100644 index 000000000..906d8a7a7 --- /dev/null +++ b/cdi/shedlock-cdi-vintage/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + shedlock-parent + net.javacrumbs.shedlock + 5.0.0-SNAPSHOT + ../../pom.xml + + + shedlock-cdi-vintage + 5.0.0-SNAPSHOT + + + + + net.javacrumbs.shedlock + shedlock-core + ${project.version} + compile + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + 2.0.2 + + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + + + + org.eclipse.microprofile.config + microprofile-config-api + 2.0.1 + + + + ch.qos.logback + logback-classic + ${logback.ver} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.ver} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.ver} + test + + + + org.mockito + mockito-core + ${mockito.ver} + test + + + org.assertj + assertj-core + ${assertj.ver} + test + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + net.javacrumbs.shedlock.cdivintage + + + + + + + + diff --git a/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/SchedulerLock.java b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/SchedulerLock.java new file mode 100644 index 000000000..1de570832 --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/SchedulerLock.java @@ -0,0 +1,38 @@ +package net.javacrumbs.shedlock.cdi; + +import javax.enterprise.util.Nonbinding; +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@InterceptorBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Inherited +public @interface SchedulerLock { + /** + * Lock name. + */ + @Nonbinding String name(); + + /** + * How long the lock should be kept in case the machine which obtained the lock died before releasing it. + * This is just a fallback, under normal circumstances the lock is released as soon the tasks finishes. Can be any format + * supported by Duration Conversion + *

+ */ + @Nonbinding String lockAtMostFor() default ""; + + /** + * The lock will be held at least for this period of time. Can be used if you really need to execute the task + * at most once in given period of time. If the duration of the task is shorter than clock difference between nodes, the task can + * be theoretically executed more than once (one node after another). By setting this parameter, you can make sure that the + * lock will be kept at least for given period of time. Can be any format + * supported by Duration Conversion + */ + @Nonbinding String lockAtLeastFor() default ""; +} + diff --git a/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/CdiLockConfigurationExtractor.java b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/CdiLockConfigurationExtractor.java new file mode 100644 index 000000000..3a5e54ab3 --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/CdiLockConfigurationExtractor.java @@ -0,0 +1,87 @@ +/** + * Copyright 2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.javacrumbs.shedlock.cdi.internal; + + +import net.javacrumbs.shedlock.cdi.SchedulerLock; +import net.javacrumbs.shedlock.core.ClockProvider; +import net.javacrumbs.shedlock.core.LockConfiguration; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; +import static net.javacrumbs.shedlock.cdi.internal.Utils.parseDuration; + +class CdiLockConfigurationExtractor { + private final Duration defaultLockAtMostFor; + private final Duration defaultLockAtLeastFor; + + CdiLockConfigurationExtractor(Duration defaultLockAtMostFor, Duration defaultLockAtLeastFor) { + this.defaultLockAtMostFor = requireNonNull(defaultLockAtMostFor); + this.defaultLockAtLeastFor = requireNonNull(defaultLockAtLeastFor); + } + + + Optional getLockConfiguration(Method method) { + Optional annotation = findAnnotation(method); + return annotation.map(this::getLockConfiguration); + } + + private LockConfiguration getLockConfiguration(SchedulerLock annotation) { + return new LockConfiguration( + ClockProvider.now(), + getName(annotation), + getLockAtMostFor(annotation), + getLockAtLeastFor(annotation) + ); + } + + private String getName(SchedulerLock annotation) { + return annotation.name(); + } + + Duration getLockAtMostFor(SchedulerLock annotation) { + return getValue( + annotation.lockAtMostFor(), + this.defaultLockAtMostFor, + "lockAtMostFor" + ); + } + + Duration getLockAtLeastFor(SchedulerLock annotation) { + return getValue( + annotation.lockAtLeastFor(), + this.defaultLockAtLeastFor, + "lockAtLeastFor" + ); + } + + private Duration getValue(String stringValueFromAnnotation, Duration defaultValue, String paramName) { + if (!stringValueFromAnnotation.isEmpty()) { + return parseDuration(stringValueFromAnnotation); + } else { + return defaultValue; + } + } + + Optional findAnnotation(Method method) { + return Optional.ofNullable(method.getAnnotation(SchedulerLock.class)); + } +} + + diff --git a/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/LockingNotSupportedException.java b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/LockingNotSupportedException.java new file mode 100644 index 000000000..7532ee44e --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/LockingNotSupportedException.java @@ -0,0 +1,24 @@ +/** + * Copyright 2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.javacrumbs.shedlock.cdi.internal; + +import net.javacrumbs.shedlock.support.LockException; + +class LockingNotSupportedException extends LockException { + LockingNotSupportedException() { + super("Can not lock method returning value (do not know what to return if it's locked)"); + } +} diff --git a/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/SchedulerLockInterceptor.java b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/SchedulerLockInterceptor.java new file mode 100644 index 000000000..129fad619 --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/SchedulerLockInterceptor.java @@ -0,0 +1,60 @@ +package net.javacrumbs.shedlock.cdi.internal; + +import net.javacrumbs.shedlock.cdi.SchedulerLock; +import net.javacrumbs.shedlock.core.DefaultLockingTaskExecutor; +import net.javacrumbs.shedlock.core.LockConfiguration; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.core.LockingTaskExecutor; +import org.eclipse.microprofile.config.ConfigProvider; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; + +import static net.javacrumbs.shedlock.cdi.internal.Utils.parseDuration; + + +@SchedulerLock(name = "?") +@Priority(3001) +@Interceptor +public class SchedulerLockInterceptor { + private final LockingTaskExecutor lockingTaskExecutor; + private final CdiLockConfigurationExtractor lockConfigurationExtractor; + + @Inject + public SchedulerLockInterceptor(LockProvider lockProvider) { + this.lockingTaskExecutor = new DefaultLockingTaskExecutor(lockProvider); + String lockAtMostFor = getConfigValue("shedlock.defaults.lock-at-most-for"); + String lockAtLeastFor = getConfigValue("shedlock.defaults.lock-at-least-for"); + Objects.requireNonNull(lockAtMostFor, "shedlock.defaults.lock-at-most-for parameter is mandatory"); + this.lockConfigurationExtractor = new CdiLockConfigurationExtractor( + parseDuration(lockAtMostFor), + lockAtLeastFor != null ? parseDuration(lockAtLeastFor) : Duration.ZERO + ); + } + + private static String getConfigValue(String propertyName) { + return ConfigProvider.getConfig().getConfigValue(propertyName).getValue(); + } + + @AroundInvoke + Object lock(InvocationContext context) throws Throwable { + Class returnType = context.getMethod().getReturnType(); + if (!void.class.equals(returnType) && !Void.class.equals(returnType)) { + throw new LockingNotSupportedException(); + } + + Optional lockConfiguration = lockConfigurationExtractor.getLockConfiguration(context.getMethod()); + if (lockConfiguration.isPresent()) { + lockingTaskExecutor.executeWithLock((LockingTaskExecutor.Task) context::proceed, lockConfiguration.get()); + return null; + } else { + return context.proceed(); + } + } +} diff --git a/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/Utils.java b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/Utils.java new file mode 100644 index 000000000..3ae189000 --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/java/net/javacrumbs/shedlock/cdi/internal/Utils.java @@ -0,0 +1,19 @@ +package net.javacrumbs.shedlock.cdi.internal; + +import java.time.Duration; +import java.time.format.DateTimeParseException; + +class Utils { + static Duration parseDuration(String value) { + value = value.trim(); + if (value.isEmpty()) { + return null; + } + + try { + return Duration.parse(value); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/cdi/shedlock-cdi-vintage/src/main/resources/META-INF/beans.xml b/cdi/shedlock-cdi-vintage/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..75b9e9cce --- /dev/null +++ b/cdi/shedlock-cdi-vintage/src/main/resources/META-INF/beans.xml @@ -0,0 +1,3 @@ + + diff --git a/cdi/test/quarkus-test/pom.xml b/cdi/test/quarkus-test/pom.xml new file mode 100644 index 000000000..e7c664771 --- /dev/null +++ b/cdi/test/quarkus-test/pom.xml @@ -0,0 +1,125 @@ + + + + shedlock-parent + net.javacrumbs.shedlock + 5.0.0-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + quarkus-test + 5.0.0-SNAPSHOT + + + quarkus-bom + io.quarkus.platform + 2.13.3.Final + true + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + net.javacrumbs.shedlock + shedlock-cdi-vintage + ${project.version} + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-scheduler + + + net.javacrumbs.shedlock + shedlock-provider-jdbc + 4.42.0 + + + org.testcontainers + postgresql + 1.17.5 + + + io.quarkus + quarkus-liquibase + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.mockito + mockito-core + ${mockito.ver} + test + + + org.assertj + assertj-core + ${assertj.ver} + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + native + + + native + + + + false + native + + + + diff --git a/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusConfig.java b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusConfig.java new file mode 100644 index 000000000..a1ba035a6 --- /dev/null +++ b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusConfig.java @@ -0,0 +1,86 @@ +/** + * Copyright 2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.javacrumbs.shedlock.quarkus.test; + + +import net.javacrumbs.shedlock.cdi.SchedulerLock; +import net.javacrumbs.shedlock.core.LockProvider; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; +import java.io.IOException; + +import static net.javacrumbs.shedlock.core.LockAssert.assertLocked; +import static org.mockito.Mockito.mock; + +public class QuarkusConfig { + + @Produces + @Singleton + public LockProvider lockProvider() { + return mock(LockProvider.class); + } + + + @ApplicationScoped + static class TestBean { + + public void noAnnotation() { + assertLocked(); + } + + @SchedulerLock(name = "normal") + public void normal() { + assertLocked(); + } + + @SchedulerLock(name = "runtimeException", lockAtMostFor = "PT0.1s") + public Void throwsRuntimeException() { + throw new RuntimeException(); + } + + @SchedulerLock(name = "exception") + public void throwsException() throws Exception { + throw new IOException(); + } + + @SchedulerLock(name = "returnsValue") + public int returnsValue() { + return 0; + } + + @SchedulerLock(name = "${property.value}", lockAtLeastFor = "${property.lock-at-least-for}") + public void property() { + + } + } + + + interface AnotherTestBean { + void runManually(); + } + + @ApplicationScoped + static class AnotherTestBeanImpl implements AnotherTestBean { + + @Override + @SchedulerLock(name = "classAnnotation") + public void runManually() { + + } + } +} diff --git a/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusShedlockTest.java b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusShedlockTest.java new file mode 100644 index 000000000..6f556634e --- /dev/null +++ b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/QuarkusShedlockTest.java @@ -0,0 +1,108 @@ +/** + * Copyright 2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.javacrumbs.shedlock.quarkus.test; + +import io.quarkus.test.junit.QuarkusTest; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.core.SimpleLock; +import net.javacrumbs.shedlock.quarkus.test.QuarkusConfig.AnotherTestBean; +import net.javacrumbs.shedlock.quarkus.test.QuarkusConfig.TestBean; +import net.javacrumbs.shedlock.support.LockException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Optional; + +import static net.javacrumbs.shedlock.quarkus.test.TestUtils.hasParams; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + + +@QuarkusTest +class QuarkusShedlockTest { + @Inject + LockProvider lockProvider; + + @Inject + TestBean testBean; + + @Inject + AnotherTestBean anotherTestBean; + + private final SimpleLock simpleLock = mock(SimpleLock.class); + + @BeforeEach + void prepareMocks() { + Mockito.reset(lockProvider, simpleLock); + when(lockProvider.lock(any())).thenReturn(Optional.of(simpleLock)); + } + + @Test + void shouldNotCallLockProviderWithNoAnnotation() { + assertThatThrownBy(() -> testBean.noAnnotation()).hasMessageStartingWith("The task is not locked."); + verifyNoInteractions(lockProvider); + } + + @Test + void shouldCallLockProviderOnDirectCall() { + testBean.normal(); + verify(lockProvider).lock(hasParams("normal", 30_000, 100)); + verify(simpleLock).unlock(); + } + + @Test + void shouldRethrowRuntimeException() { + assertThatThrownBy(() -> testBean.throwsRuntimeException()).isInstanceOf(RuntimeException.class); + verify(lockProvider).lock(hasParams("runtimeException", 100, 100)); + verify(simpleLock).unlock(); + } + + @Test + void shouldRethrowDeclaredException() { + assertThatThrownBy(() -> testBean.throwsException()).isInstanceOf(IOException.class); + verify(lockProvider).lock(hasParams("exception", 30_000, 100)); + verify(simpleLock).unlock(); + } + + @Test + void shouldFailOnReturnType() { + assertThatThrownBy(() -> testBean.returnsValue()).isInstanceOf(LockException.class); + verifyNoInteractions(lockProvider); + } + + @Test + @Disabled // Not implemented, waiting if anyone is going to use it. When needed, get the code from Quarkus SchedulerUtils + void shouldReadConfigurationProperty() { + testBean.property(); + verify(lockProvider).lock(hasParams("property", 30_000, 1_000)); + verify(simpleLock).unlock(); + } + + @Test + void shouldReadAnnotationFromImplementationClass() { + anotherTestBean.runManually(); + verify(lockProvider).lock(hasParams("classAnnotation", 30_000, 100)); + verify(simpleLock).unlock(); + } +} diff --git a/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/TestUtils.java b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/TestUtils.java new file mode 100644 index 000000000..5a76b6c19 --- /dev/null +++ b/cdi/test/quarkus-test/src/test/java/net/javacrumbs/shedlock/quarkus/test/TestUtils.java @@ -0,0 +1,61 @@ +/** + * Copyright 2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.javacrumbs.shedlock.quarkus.test; + +import net.javacrumbs.shedlock.core.ClockProvider; +import net.javacrumbs.shedlock.core.LockConfiguration; +import org.mockito.ArgumentMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; + +import static org.mockito.ArgumentMatchers.argThat; + +public class TestUtils { + + private static final int GAP = 1000; + + private static final Logger logger = LoggerFactory.getLogger(TestUtils.class); + + public static LockConfiguration hasParams(String name, long lockAtMostFor, long lockAtLeastFor) { + return argThat(new ArgumentMatcher<>() { + @Override + public boolean matches(LockConfiguration c) { + return name.equals(c.getName()) + && isNearTo(lockAtMostFor, c.getLockAtMostUntil()) + && isNearTo(lockAtLeastFor, c.getLockAtLeastUntil()); + } + + @Override + public String toString() { + Instant now = ClockProvider.now(); + return "hasParams(\"" + name + "\", " + now.plusMillis(lockAtMostFor) + ", " + now.plusMillis(lockAtLeastFor) + ")"; + } + }); + } + + private static boolean isNearTo(long expected, Instant time) { + Instant now = ClockProvider.now(); + Instant from = now.plusMillis(expected - GAP); + Instant to = now.plusMillis(expected); + boolean isNear = time.isAfter(from) && !time.isAfter(to); + if (!isNear) { + logger.info("Assertion failed time={} is not between {} and {}", time, from, to); + } + return isNear; + } +} diff --git a/cdi/test/quarkus-test/src/test/resources/application.properties b/cdi/test/quarkus-test/src/test/resources/application.properties new file mode 100644 index 000000000..c583b5fbd --- /dev/null +++ b/cdi/test/quarkus-test/src/test/resources/application.properties @@ -0,0 +1 @@ +shedlock.defaults.lock-at-most-for=PT30S diff --git a/pom.xml b/pom.xml index a2d68ee70..61ca55d13 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,8 @@ shedlock-bom shedlock-core + cdi/shedlock-cdi-vintage + cdi/test/quarkus-test micronaut/shedlock-micronaut micronaut/test/micronaut-jdbc micronaut/test/micronaut-jdbc-template