diff --git a/.gitignore b/.gitignore index f8603a0d8..8dab6ac73 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ .attach_pid* micronaut/micronaut-test/dependency-reduced-pom.xml dependency-reduced-pom.xml +.vscode/settings.json diff --git a/pom.xml b/pom.xml index a98263772..bb21a5cd5 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ providers/zookeeper/shedlock-provider-zookeeper-curator providers/redis/shedlock-provider-redis-jedis4 providers/redis/shedlock-provider-redis-spring + providers/redis/shedlock-provider-redis-quarkus2 providers/dynamodb/shedlock-provider-dynamodb2 providers/cassandra/shedlock-provider-cassandra providers/consul/shedlock-provider-consul diff --git a/providers/redis/shedlock-provider-redis-quarkus2/pom.xml b/providers/redis/shedlock-provider-redis-quarkus2/pom.xml new file mode 100644 index 000000000..2e272d482 --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/pom.xml @@ -0,0 +1,118 @@ + + + + shedlock-parent + net.javacrumbs.shedlock + 5.8.1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + shedlock-provider-redis-quarkus2 + + + 2.16.11.Final + true + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + + + + + net.javacrumbs.shedlock + shedlock-core + ${project.version} + + + + net.javacrumbs.shedlock + shedlock-test-support + ${project.version} + test + + + + net.javacrumbs.shedlock + shedlock-cdi-vintage + ${project.version} + + + + io.quarkus + quarkus-scheduler + + + io.quarkus + quarkus-redis-client + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + net.javacrumbs.shedlock.provider.redis.quarkus + + + + + + + + + + native + + + native + + + + false + native + + + + + \ No newline at end of file diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/QuarkusRedisLockProvider.java b/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/QuarkusRedisLockProvider.java new file mode 100644 index 000000000..976837fdd --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/QuarkusRedisLockProvider.java @@ -0,0 +1,145 @@ +package net.javacrumbs.shedlock.provider.redis.quarkus2; + +import static net.javacrumbs.shedlock.support.Utils.getHostname; +import static net.javacrumbs.shedlock.support.Utils.toIsoString; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.keys.KeyCommands; +import io.quarkus.redis.datasource.value.SetArgs; +import io.quarkus.redis.datasource.value.ValueCommands; +import io.quarkus.runtime.configuration.ConfigUtils; +import net.javacrumbs.shedlock.core.AbstractSimpleLock; +import net.javacrumbs.shedlock.core.ClockProvider; +import net.javacrumbs.shedlock.core.ExtensibleLockProvider; +import net.javacrumbs.shedlock.core.LockConfiguration; +import net.javacrumbs.shedlock.core.SimpleLock; +import net.javacrumbs.shedlock.support.LockException; +import net.javacrumbs.shedlock.support.annotation.NonNull; + +/** + * Uses Redis's `SET resource-name anystring NX EX max-lock-ms-time` as locking mechanism. + *

+ * See Set command + */ +public class QuarkusRedisLockProvider implements ExtensibleLockProvider { + + private static final Logger LOG = LoggerFactory.getLogger(QuarkusRedisLockProvider.class); + + private static final String KEY_PREFIX = "lock"; + + private final RedisDataSource redisDataSource; + private final ValueCommands valueCommands; + private final KeyCommands keyCommands; + private final String environment; + + private boolean throwsException; + + public QuarkusRedisLockProvider(RedisDataSource dataSource, String appNameOrPrefix, boolean throwsException) { + this.redisDataSource = dataSource; + this.throwsException = throwsException; + this.environment = appNameOrPrefix + ":"+ String.join(":", ConfigUtils.getProfiles()); + this.valueCommands = redisDataSource.value(String.class); + this.keyCommands = redisDataSource.key(String.class); + } + + + @Override + @NonNull + public Optional lock(@NonNull LockConfiguration lockConfiguration) { + + long expireTime = getMillisUntil(lockConfiguration.getLockAtMostUntil()); + + String key = buildKey(lockConfiguration.getName(), this.environment); + + String value = valueCommands.setGet(key, buildValue(), new SetArgs().nx().px(expireTime)); + if(value != null) { + if(throwsException) throw new LockException("Already locked !"); + return Optional.empty(); + }else { + return Optional.of(new RedisLock(key, this, lockConfiguration)); + } + + } + + private Optional extend(LockConfiguration lockConfiguration) { + + long expireTime = getMillisUntil(lockConfiguration.getLockAtMostUntil()); + + String key = buildKey(lockConfiguration.getName(), this.environment); + + boolean success = extendKeyExpiration(key, expireTime); + + if (success) { + return Optional.of(new RedisLock(key, this, lockConfiguration)); + } + + return Optional.empty(); + } + + + private boolean extendKeyExpiration(String key, long expiration) { + String value = valueCommands.setGet(key, buildValue(), new SetArgs().xx().px(expiration)); + return value != null; + + } + + private void deleteKey(String key) { + keyCommands.del(key); + } + + + private static final class RedisLock extends AbstractSimpleLock { + private final String key; + private final QuarkusRedisLockProvider quarkusLockProvider; + + private RedisLock(String key, QuarkusRedisLockProvider jedisLockProvider, LockConfiguration lockConfiguration) { + super(lockConfiguration); + this.key = key; + this.quarkusLockProvider = jedisLockProvider; + } + + @Override + public void doUnlock() { + long keepLockFor = getMillisUntil(lockConfiguration.getLockAtLeastUntil()); + + // lock at least until is in the past + if (keepLockFor <= 0) { + try { + quarkusLockProvider.deleteKey(key); + } catch (Exception e) { + throw new LockException("Can not remove key", e); + } + } else { + quarkusLockProvider.extendKeyExpiration(key, keepLockFor); + } + } + + @Override + @NonNull + protected Optional doExtend(@NonNull LockConfiguration newConfiguration) { + return quarkusLockProvider.extend(newConfiguration); + } + } + + private static long getMillisUntil(Instant instant) { + return Duration.between(ClockProvider.now(), instant).toMillis(); + } + + static String buildKey(String lockName, String env) { + return String.format("%s:%s:%s", KEY_PREFIX, env, lockName); + } + + private static String buildValue() { + return String.format("ADDED:%s@%s", toIsoString(ClockProvider.now()), getHostname()); + } + + + +} \ No newline at end of file diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/SchedulerLockFactory.java b/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/SchedulerLockFactory.java new file mode 100644 index 000000000..240cf0df5 --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/main/java/net/javacrumbs/shedlock/provider/redis/quarkus2/SchedulerLockFactory.java @@ -0,0 +1,28 @@ +package net.javacrumbs.shedlock.provider.redis.quarkus2; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.quarkus.redis.datasource.RedisDataSource; +import net.javacrumbs.shedlock.core.LockProvider; + + +@ApplicationScoped +public class SchedulerLockFactory { + + @ConfigProperty(name = "quarkus.application.name") + String app; + + @ConfigProperty(name = "shedlock.quarkus.throws-exception-if-locked", defaultValue = "false") + boolean throwsException; + + @Produces + @Singleton + public LockProvider lockProvider(RedisDataSource redisDataSource) { + + return new QuarkusRedisLockProvider(redisDataSource, app, throwsException); + } +} diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/LockedService.java b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/LockedService.java new file mode 100644 index 000000000..8645d45cf --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/LockedService.java @@ -0,0 +1,66 @@ +package net.javacrumbs.shedlock.provider.redis.quarkus; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.enterprise.context.ApplicationScoped; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.smallrye.common.annotation.Blocking; +import net.javacrumbs.shedlock.cdi.SchedulerLock; + +@ApplicationScoped +public class LockedService{ + + private static final Logger LOG = LoggerFactory.getLogger(LockedService.class); + + private AtomicInteger count = new AtomicInteger(0); + + @SchedulerLock(name = "test") + public void test(int time) { + + execute(time); + + LOG.info("Executing [DONE]"); + + } + + public void execute(int time) { + count.incrementAndGet(); + LOG.info("Executing ....(c="+count.get()+")"); + + try { + TimeUnit.MILLISECONDS.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + + @SchedulerLock(name = "testException") + @Blocking + public void testException() { + + try { + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + + throw new RuntimeException("test"); + + } + + public int count() { + LOG.info("getc=("+count.get()+")"); + return count.get(); + } + + public void countReset() { + count.set(0); + } + +} diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderExceptionsTest.java b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderExceptionsTest.java new file mode 100644 index 000000000..6e93a5d3a --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderExceptionsTest.java @@ -0,0 +1,111 @@ +package net.javacrumbs.shedlock.provider.redis.quarkus; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import net.javacrumbs.shedlock.support.LockException; + +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestProfile(QuarkusRedisLockProviderExceptionsTest.MyProfile.class) +public class QuarkusRedisLockProviderExceptionsTest { + + public static class MyProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("shedlock.quarkus.throws-exception-if-locked", "true"); + } + } + + @Inject + LockedService lockedService; + + @Inject + RedisDataSource dataSource; + + private ExecutorService executorService = Executors.newFixedThreadPool(5); + + @AfterEach + public void afterEach() { + lockedService.countReset(); + try { + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Test + @Order(1) + void test_warmUp() throws Exception { + lockedService.test(100); + assertEquals(1, lockedService.count()); + } + + @Test + void test_sequenceCalls() throws Exception { + lockedService.test(50); + lockedService.test(50); + assertEquals(2, lockedService.count()); + } + + @Test + void test_basicLock_withExcepions() throws Exception { + + executorService.execute(() -> { lockedService.test(300); }); + Thread.sleep(100); + assertTrue(isLockExist("test"), this::lockMessage); + + assertThrows(LockException.class, () -> { + lockedService.test(300); + }); + + + Thread.sleep(300); // wait to release lock and verify + assertFalse(isLockExist("test"), this::lockMessage); + assertEquals(1, lockedService.count()); + + } + + + @Test + void test_highConcurrency() throws Exception { + + for (int i = 0; i < 100; i++) { + executorService.execute(() -> { lockedService.test(2000); }); + Thread.sleep(10); + } + + assertTrue(isLockExist("test"), this::lockMessage); + assertEquals(1, lockedService.count()); + } + + + private boolean isLockExist(String name) { + return dataSource.key().exists("lock:my-app:test:" + name); + } + + private String lockMessage() { + return "Current Keys: " + dataSource.key().keys("*"); + } + +} diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderIntegrationTest.java b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderIntegrationTest.java new file mode 100644 index 000000000..50030b3a0 --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/test/java/net/javacrumbs/shedlock/provider/redis/quarkus/QuarkusRedisLockProviderIntegrationTest.java @@ -0,0 +1,59 @@ +package net.javacrumbs.shedlock.provider.redis.quarkus; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.inject.Inject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.value.ValueCommands; +import io.quarkus.test.junit.QuarkusTest; +import net.javacrumbs.shedlock.core.ExtensibleLockProvider; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.test.support.AbstractExtensibleLockProviderIntegrationTest; + + +@QuarkusTest +public class QuarkusRedisLockProviderIntegrationTest extends AbstractExtensibleLockProviderIntegrationTest { + + @Inject + LockProvider lockProvider; + + @Inject + RedisDataSource dataSource; + + private ValueCommands values; + + @BeforeEach + public void beforeEach() { + this.values = dataSource.value(String.class); + } + + @Test + void warmUp() throws Exception { + this.values.getDataSource(); + } + + @Override + protected void assertUnlocked(String lockName) { + assertThat(getLock(lockName)).isNull(); + } + + @Override + protected void assertLocked(String lockName) { + assertThat(getLock(lockName)).isNotNull(); + } + + private String getLock(String lockName) { + return values.get("lock:my-app:test:"+lockName); + } + + @Override + protected ExtensibleLockProvider getLockProvider() { + return (ExtensibleLockProvider) lockProvider; + } + + +} diff --git a/providers/redis/shedlock-provider-redis-quarkus2/src/test/resources/application.properties b/providers/redis/shedlock-provider-redis-quarkus2/src/test/resources/application.properties new file mode 100644 index 000000000..e837805ef --- /dev/null +++ b/providers/redis/shedlock-provider-redis-quarkus2/src/test/resources/application.properties @@ -0,0 +1,10 @@ +quarkus.application.name=my-app + +shedlock.defaults.lock-at-most-for=PT1h +shedlock.quarkus.throws-exception-if-locked=false + +#Logs +quarkus.log.console.format=%d{dd-MM-yyyy HH:mm:ss.SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.level=INFO +quarkus.log.category."org.testcontainers".level=INFO +