readValue(InputStream is) throws IOException {
+ return this.reader.readValue(is);
+ }
+}
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
index a52ff559827ff..47a397108eaa1 100644
--- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
@@ -303,6 +303,22 @@ InfinispanPropertiesBuildItem setup(ApplicationArchivesBuildItem applicationArch
"org.infinispan.client.hotrod.impl.consistenthash.SegmentConsistentHash")
.build());
+ // Elytron Classes
+ String[] elytronClasses = new String[] {
+ "org.wildfly.security.sasl.plain.PlainSaslClientFactory",
+ "org.wildfly.security.sasl.scram.ScramSaslClientFactory",
+ "org.wildfly.security.credential.BearerTokenCredential",
+ "org.wildfly.security.credential.GSSKerberosCredential",
+ "org.wildfly.security.credential.KeyPairCredential",
+ "org.wildfly.security.credential.PasswordCredential",
+ "org.wildfly.security.credential.PublicKeyCredential",
+ "org.wildfly.security.credential.SecretKeyCredential",
+ "org.wildfly.security.credential.SSHCredential",
+ "org.wildfly.security.credential.X509CertificateChainPrivateCredential",
+ "org.wildfly.security.credential.X509CertificateChainPublicCredential"
+ };
+
+ reflectiveClass.produce(ReflectiveClassBuildItem.builder(elytronClasses).build());
return new InfinispanPropertiesBuildItem(propertiesMap);
}
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java
similarity index 98%
rename from extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java
rename to extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java
index 95d4d47724c53..38b42d218c2a6 100644
--- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java
@@ -1,4 +1,4 @@
-package io.quarkus.quartz.test;
+package io.quarkus.quartz.test.timezone;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java
new file mode 100644
index 0000000000000..4e588223254a3
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java
@@ -0,0 +1,74 @@
+package io.quarkus.quartz.test.timezone;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.scheduler.ScheduledExecution;
+import io.quarkus.scheduler.Scheduler;
+import io.quarkus.scheduler.Trigger;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class TriggerNextFireTimeZoneTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(Jobs.class);
+ });
+
+ @Inject
+ Scheduler scheduler;
+
+ @Test
+ public void testScheduledJobs() throws InterruptedException {
+ Trigger prague = scheduler.getScheduledJob("prague");
+ Trigger boston = scheduler.getScheduledJob("boston");
+ Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar");
+ assertNotNull(prague);
+ assertNotNull(boston);
+ assertNotNull(ulaanbaatar);
+ Instant pragueNext = prague.getNextFireTime();
+ Instant bostonNext = boston.getNextFireTime();
+ Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime();
+ assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague")));
+ assertTime(bostonNext.atZone(ZoneId.of("America/New_York")));
+ assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar")));
+ }
+
+ private static void assertTime(ZonedDateTime time) {
+ assertEquals(20, time.getHour());
+ assertEquals(30, time.getMinute());
+ assertEquals(0, time.getSecond());
+ }
+
+ static class Jobs {
+
+ @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague")
+ void withPragueTimezone(ScheduledExecution execution) {
+ assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime());
+ assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague")));
+ }
+
+ @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York")
+ void withBostonTimezone() {
+ }
+
+ @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar")
+ void withIstanbulTimezone(ScheduledExecution execution) {
+ assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar")));
+ }
+
+ }
+
+}
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java
new file mode 100644
index 0000000000000..4da6a548d7e43
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java
@@ -0,0 +1,93 @@
+package io.quarkus.quartz.test.timezone;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.scheduler.Scheduler;
+import io.quarkus.scheduler.Trigger;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class TriggerPrevFireTimeZoneTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague"));
+ ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul"));
+ // For example, the current date-time is 2024-07-09 10:08:00;
+ // the default time zone is Europe/London
+ // then the config should look like:
+ // simpleJobs1.cron=0/1 * 11 * * ?
+ // simpleJobs2.cron=0/1 * 12 * * ?
+ String properties = String.format(
+ "simpleJobs1.cron=0/1 * %s * * ?\n"
+ + "simpleJobs1.hour=%s\n"
+ + "simpleJobs2.cron=0/1 * %s * * ?\n"
+ + "simpleJobs2.hour=%s",
+ prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour());
+ root.addClasses(Jobs.class)
+ .addAsResource(
+ new StringAsset(properties),
+ "application.properties");
+ });
+
+ @ConfigProperty(name = "simpleJobs1.hour")
+ int pragueHour;
+
+ @ConfigProperty(name = "simpleJobs2.hour")
+ int istanbulHour;
+
+ @Inject
+ Scheduler scheduler;
+
+ @Test
+ public void testScheduledJobs() throws InterruptedException {
+ assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS));
+ Trigger prague = scheduler.getScheduledJob("prague");
+ Trigger istanbul = scheduler.getScheduledJob("istanbul");
+ assertNotNull(prague);
+ assertNotNull(istanbul);
+ Instant praguePrev = prague.getPreviousFireTime();
+ Instant istanbulPrev = istanbul.getPreviousFireTime();
+ assertNotNull(praguePrev);
+ assertNotNull(istanbulPrev);
+ assertEquals(praguePrev, istanbulPrev);
+ assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour());
+ assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour());
+ }
+
+ static class Jobs {
+
+ static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1);
+ static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1);
+
+ @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague")
+ void withPragueTimezone() {
+ PRAGUE_LATCH.countDown();
+ }
+
+ @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul")
+ void withIstanbulTimezone() {
+ ISTANBUL_LATCH.countDown();
+ }
+
+ }
+
+}
diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java
index 5f69240af667c..fda1ac26fec88 100644
--- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java
+++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java
@@ -14,6 +14,9 @@ public interface ScheduledExecution {
Trigger getTrigger();
/**
+ * The returned {@code Instant} is converted from the date-time in the default timezone. A timezone of a cron-based job
+ * is not taken into account.
+ *
* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value.
*
* @return the time the associated trigger was fired
@@ -21,6 +24,12 @@ public interface ScheduledExecution {
Instant getFireTime();
/**
+ * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
+ * account.
+ *
+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin},
+ * then the return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for
+ * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}.
*
* @return the time the action was scheduled for
*/
diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java
index c076e5712bc0e..0a5f94d48ffb3 100644
--- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java
+++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java
@@ -21,12 +21,24 @@ public interface Trigger {
String getId();
/**
+ * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
+ * account.
+ *
+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the
+ * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for
+ * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}.
*
* @return the next time at which the trigger is scheduled to fire, or {@code null} if it will not fire again
*/
Instant getNextFireTime();
/**
+ * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
+ * account.
+ *
+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the
+ * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for
+ * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}.
*
* @return the previous time at which the trigger fired, or {@code null} if it has not fired yet
*/
diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java
similarity index 97%
rename from extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java
rename to extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java
index aba645216812a..9db547b46e7a0 100644
--- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java
+++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java
@@ -1,4 +1,4 @@
-package io.quarkus.scheduler.test;
+package io.quarkus.scheduler.test.timezone;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -41,7 +41,6 @@ public class ScheduledMethodTimeZoneTest {
+ "simpleJobs2.cron=0/1 * %s * * ?\n"
+ "simpleJobs2.timeZone=%s",
now.getHour(), timeZone, job2Hour, timeZone);
- // System.out.println(properties);
jar.addClasses(Jobs.class)
.addAsResource(
new StringAsset(properties),
diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java
new file mode 100644
index 0000000000000..d301a0e1cff5d
--- /dev/null
+++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java
@@ -0,0 +1,74 @@
+package io.quarkus.scheduler.test.timezone;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.scheduler.ScheduledExecution;
+import io.quarkus.scheduler.Scheduler;
+import io.quarkus.scheduler.Trigger;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class TriggerNextFireTimeZoneTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(Jobs.class);
+ });
+
+ @Inject
+ Scheduler scheduler;
+
+ @Test
+ public void testScheduledJobs() throws InterruptedException {
+ Trigger prague = scheduler.getScheduledJob("prague");
+ Trigger boston = scheduler.getScheduledJob("boston");
+ Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar");
+ assertNotNull(prague);
+ assertNotNull(boston);
+ assertNotNull(ulaanbaatar);
+ Instant pragueNext = prague.getNextFireTime();
+ Instant bostonNext = boston.getNextFireTime();
+ Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime();
+ assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague")));
+ assertTime(bostonNext.atZone(ZoneId.of("America/New_York")));
+ assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar")));
+ }
+
+ private static void assertTime(ZonedDateTime time) {
+ assertEquals(20, time.getHour());
+ assertEquals(30, time.getMinute());
+ assertEquals(0, time.getSecond());
+ }
+
+ static class Jobs {
+
+ @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague")
+ void withPragueTimezone(ScheduledExecution execution) {
+ assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime());
+ assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague")));
+ }
+
+ @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York")
+ void withBostonTimezone() {
+ }
+
+ @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar")
+ void withIstanbulTimezone(ScheduledExecution execution) {
+ assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar")));
+ }
+
+ }
+
+}
diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java
new file mode 100644
index 0000000000000..ed1ef873b77b4
--- /dev/null
+++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java
@@ -0,0 +1,93 @@
+package io.quarkus.scheduler.test.timezone;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.scheduler.Scheduler;
+import io.quarkus.scheduler.Trigger;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class TriggerPrevFireTimeZoneTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague"));
+ ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul"));
+ // For example, the current date-time is 2024-07-09 10:08:00;
+ // the default time zone is Europe/London
+ // then the config should look like:
+ // simpleJobs1.cron=0/1 * 11 * * ?
+ // simpleJobs2.cron=0/1 * 12 * * ?
+ String properties = String.format(
+ "simpleJobs1.cron=0/1 * %s * * ?\n"
+ + "simpleJobs1.hour=%s\n"
+ + "simpleJobs2.cron=0/1 * %s * * ?\n"
+ + "simpleJobs2.hour=%s",
+ prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour());
+ root.addClasses(Jobs.class)
+ .addAsResource(
+ new StringAsset(properties),
+ "application.properties");
+ });
+
+ @ConfigProperty(name = "simpleJobs1.hour")
+ int pragueHour;
+
+ @ConfigProperty(name = "simpleJobs2.hour")
+ int istanbulHour;
+
+ @Inject
+ Scheduler scheduler;
+
+ @Test
+ public void testScheduledJobs() throws InterruptedException {
+ assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS));
+ Trigger prague = scheduler.getScheduledJob("prague");
+ Trigger istanbul = scheduler.getScheduledJob("istanbul");
+ assertNotNull(prague);
+ assertNotNull(istanbul);
+ Instant praguePrev = prague.getPreviousFireTime();
+ Instant istanbulPrev = istanbul.getPreviousFireTime();
+ assertNotNull(praguePrev);
+ assertNotNull(istanbulPrev);
+ assertEquals(praguePrev, istanbulPrev);
+ assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour());
+ assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour());
+ }
+
+ static class Jobs {
+
+ static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1);
+ static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1);
+
+ @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague")
+ void withPragueTimezone() {
+ PRAGUE_LATCH.countDown();
+ }
+
+ @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul")
+ void withIstanbulTimezone() {
+ ISTANBUL_LATCH.countDown();
+ }
+
+ }
+
+}
diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java
index ce6d7482341da..687c081bbfe50 100644
--- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java
+++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java
@@ -589,28 +589,29 @@ static class CronTrigger extends SimpleTrigger {
super(id, start, description);
this.cron = cron;
this.executionTime = ExecutionTime.forCron(cron);
- this.lastFireTime = start;
this.gracePeriod = gracePeriod;
this.timeZone = timeZone;
+ // The last fire time stores the zoned time
+ this.lastFireTime = zoned(start);
}
@Override
public Instant getNextFireTime() {
- Optional nextFireTime = executionTime.nextExecution(lastFireTime);
- return nextFireTime.isPresent() ? nextFireTime.get().toInstant() : null;
+ return executionTime.nextExecution(lastFireTime).map(ZonedDateTime::toInstant).orElse(null);
}
+ @Override
ZonedDateTime evaluate(ZonedDateTime now) {
if (now.isBefore(start)) {
return null;
}
- ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone);
- Optional lastExecution = executionTime.lastExecution(zonedNow);
+ now = zoned(now);
+ Optional lastExecution = executionTime.lastExecution(now);
if (lastExecution.isPresent()) {
ZonedDateTime lastTruncated = lastExecution.get().truncatedTo(ChronoUnit.SECONDS);
- if (zonedNow.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) {
+ if (now.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) {
LOG.tracef("%s fired, last=%s", this, lastTruncated);
- lastFireTime = zonedNow;
+ lastFireTime = now;
return lastTruncated;
}
}
@@ -623,9 +624,9 @@ public boolean isOverdue() {
if (now.isBefore(start)) {
return false;
}
- ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone);
+ now = zoned(now);
Optional nextFireTime = executionTime.nextExecution(lastFireTime);
- return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(zonedNow);
+ return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(now);
}
@Override
@@ -634,6 +635,10 @@ public String toString() {
+ timeZone + "]";
}
+ private ZonedDateTime zoned(ZonedDateTime time) {
+ return timeZone == null ? time : time.withZoneSameInstant(timeZone);
+ }
+
}
static class SimpleScheduledExecution implements ScheduledExecution {
diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java
new file mode 100644
index 0000000000000..6fd84159bd3f4
--- /dev/null
+++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java
@@ -0,0 +1,65 @@
+package io.quarkus.smallrye.health.test;
+
+import static org.hamcrest.Matchers.is;
+
+import org.eclipse.microprofile.health.HealthCheck;
+import org.eclipse.microprofile.health.HealthCheckResponse;
+import org.eclipse.microprofile.health.Liveness;
+import org.jboss.shrinkwrap.api.asset.EmptyAsset;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import io.restassured.parsing.Parser;
+import io.vertx.core.Context;
+import io.vertx.core.Vertx;
+
+class BlockingChecksVertxContextDuplicationTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(ContextCaptureCheck1.class, ContextCaptureCheck2.class)
+ .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));
+
+ @Test
+ void testBlockingChecksPropagateVertxContext() {
+ try {
+ RestAssured.defaultParser = Parser.JSON;
+ RestAssured.when().get("/q/health").then()
+ .body("status", is("UP"),
+ "checks.size()", is(2));
+
+ Assertions.assertNotEquals(ContextCaptureCheck1.capturedContext, ContextCaptureCheck2.capturedContext,
+ "Expected different contexts to be propagated into different blocking health checks");
+ } finally {
+ RestAssured.reset();
+ }
+ }
+
+ @Liveness
+ public static class ContextCaptureCheck1 implements HealthCheck {
+
+ public static Context capturedContext = null;
+
+ @Override
+ public HealthCheckResponse call() {
+ capturedContext = Vertx.currentContext();
+ return HealthCheckResponse.up("ContextCaptureCheck1");
+ }
+ }
+
+ @Liveness
+ public static class ContextCaptureCheck2 implements HealthCheck {
+
+ public static Context capturedContext = null;
+
+ @Override
+ public HealthCheckResponse call() {
+ capturedContext = Vertx.currentContext();
+ return HealthCheckResponse.up("ContextCaptureCheck2");
+ }
+ }
+}
diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java
index cfec52c933b1b..bd7c236dce2e2 100644
--- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java
+++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java
@@ -1,14 +1,19 @@
package io.quarkus.smallrye.health.runtime;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
import jakarta.inject.Singleton;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
+import io.smallrye.common.vertx.VertxContext;
import io.smallrye.health.AsyncHealthCheckFactory;
import io.smallrye.health.api.AsyncHealthCheck;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.vertx.MutinyHelper;
+import io.vertx.core.Context;
import io.vertx.core.Vertx;
/**
@@ -27,7 +32,19 @@ public QuarkusAsyncHealthCheckFactory(Vertx vertx) {
@Override
public Uni callSync(HealthCheck healthCheck) {
Uni healthCheckResponseUni = super.callSync(healthCheck);
- return healthCheckResponseUni.runSubscriptionOn(MutinyHelper.blockingExecutor(vertx, false));
+ return healthCheckResponseUni.runSubscriptionOn(new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ Context duplicatedContext = VertxContext.createNewDuplicatedContext(vertx.getOrCreateContext());
+ duplicatedContext.executeBlocking(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ command.run();
+ return null;
+ }
+ }, false);
+ }
+ });
}
@Override
diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java
index cc0bb85cce758..c35577a1d37d7 100644
--- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java
+++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java
@@ -63,7 +63,7 @@ private void doHandle(RoutingContext ctx, ManagedContext requestContext) {
.set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8")
.set(HttpHeaders.CACHE_CONTROL, "no-store");
Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks
- try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) {
+ try (BufferOutputStream outputStream = new BufferOutputStream(buffer)) {
reporter.reportHealth(outputStream, health);
resp.end(buffer);
} catch (IOException e) {
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
index 26bf46d6421a8..12a2b327fa6b1 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
@@ -291,7 +291,7 @@ private static boolean isSecurityFailure(Throwable throwable) {
|| throwable instanceof ForbiddenException;
}
- private static boolean isWebSocketIsClosedFailure(Throwable throwable, WebSocketConnectionBase connection) {
+ static boolean isWebSocketIsClosedFailure(Throwable throwable, WebSocketConnectionBase connection) {
if (!connection.isClosed()) {
return false;
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
index d1d4cad07638e..de23dd4779d78 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
@@ -219,18 +219,26 @@ public Uni sendPong(Buffer data) {
throw new UnsupportedOperationException();
}
- private Uni doSend(BiFunction> function, M message) {
+ private Uni doSend(BiFunction> sendFunction, M message) {
Set connections = connectionManager.getConnections(generatedEndpointClass);
if (connections.isEmpty()) {
return Uni.createFrom().voidItem();
}
List> unis = new ArrayList<>(connections.size());
for (WebSocketConnection connection : connections) {
- if (connection.isOpen() && (filter == null || filter.test(connection))) {
- unis.add(function.apply(connection, message));
+ if (connection.isOpen()
+ && (filter == null || filter.test(connection))) {
+ unis.add(sendFunction.apply(connection, message)
+ // Intentionally ignore 'WebSocket is closed' failures
+ // It might happen that the connection is closed in the mean time
+ .onFailure(t -> Endpoints.isWebSocketIsClosedFailure(t, (WebSocketConnectionBase) connection))
+ .recoverWithNull());
}
}
- return unis.isEmpty() ? Uni.createFrom().voidItem() : Uni.join().all(unis).andFailFast().replaceWithVoid();
+ if (unis.isEmpty()) {
+ return Uni.createFrom().voidItem();
+ }
+ return Uni.join().all(unis).andCollectFailures().replaceWithVoid();
}
}
diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java
index 0c4006fc3dc74..18513222b7384 100644
--- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java
+++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java
@@ -1,21 +1,23 @@
package io.quarkus.bootstrap.util;
-import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.StringWriter;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.SecureDirectoryStream;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.Objects;
@@ -29,8 +31,6 @@
*/
public class IoUtils {
- private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
-
private static final Path TMP_DIR = Paths.get(PropertyUtils.getProperty("java.io.tmpdir"));
private static final Logger log = Logger.getLogger(IoUtils.class);
@@ -60,40 +60,36 @@ public static Path mkdirs(Path dir) {
return dir;
}
+ /**
+ * Recursively delete the file or directory given by {@code root}.
+ * The implementation will attempt to do so in a secure manner.
+ * Any problems encountered will be logged at {@code DEBUG} level.
+ *
+ * @param root the root path (must not be {@code null})
+ */
public static void recursiveDelete(Path root) {
- log.debugf("Recursively delete directory %s", root);
+ log.debugf("Recursively delete path %s", root);
if (root == null || !Files.exists(root)) {
return;
}
try {
- Files.walkFileTree(root, new SimpleFileVisitor() {
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
- throws IOException {
- try {
- Files.delete(file);
- } catch (IOException ex) {
- log.debugf(ex, "Unable to delete file " + file);
- }
- return FileVisitResult.CONTINUE;
+ if (Files.isDirectory(root)) {
+ try (DirectoryStream ds = Files.newDirectoryStream(root)) {
+ recursiveDelete(ds);
}
-
- @Override
- public FileVisitResult postVisitDirectory(Path dir, IOException e)
- throws IOException {
- if (e == null) {
- try {
- Files.delete(dir);
- } catch (IOException ex) {
- log.debugf(ex, "Unable to delete directory " + dir);
- }
- return FileVisitResult.CONTINUE;
- } else {
- // directory iteration failed
- throw e;
- }
+ try {
+ Files.delete(root);
+ } catch (IOException e) {
+ log.debugf(e, "Unable to delete directory %s", root);
}
- });
+ } else {
+ log.debugf("Delete file %s", root);
+ try {
+ Files.delete(root);
+ } catch (IOException e) {
+ log.debugf(e, "Unable to delete file %s", root);
+ }
+ }
} catch (IOException e) {
log.debugf(e, "Error recursively deleting directory");
}
@@ -101,9 +97,10 @@ public FileVisitResult postVisitDirectory(Path dir, IOException e)
/**
* Creates a new empty directory or empties an existing one.
+ * Any problems encountered while emptying the directory will be logged at {@code DEBUG} level.
*
* @param dir directory
- * @throws IOException in case of a failure
+ * @throws IOException if creating or accessing the directory itself fails
*/
public static void createOrEmptyDir(Path dir) throws IOException {
log.debugf("Create or empty directory %s", dir);
@@ -113,17 +110,51 @@ public static void createOrEmptyDir(Path dir) throws IOException {
Files.createDirectories(dir);
return;
}
- if (!Files.isDirectory(dir)) {
- throw new IllegalArgumentException(dir + " is not a directory");
+ // recursively delete the *contents* of the directory, if any (keep the directory itself)
+ try (DirectoryStream ds = Files.newDirectoryStream(dir)) {
+ recursiveDelete(ds);
+ }
+ }
+
+ private static void recursiveDelete(DirectoryStream ds) {
+ if (ds instanceof SecureDirectoryStream sds) {
+ // best, fastest, and most likely path for most OSes
+ recursiveDeleteSecure(sds);
+ } else {
+ // this may not work well on e.g. NFS, so we avoid this path if possible
+ for (Path p : ds) {
+ recursiveDelete(p);
+ }
}
- log.debugf("Iterate over contents of %s to delete its contents", dir);
- try (DirectoryStream stream = Files.newDirectoryStream(dir)) {
- for (Path p : stream) {
- if (Files.isDirectory(p)) {
- recursiveDelete(p);
- } else {
- log.debugf("Delete file %s", p);
- Files.delete(p);
+ }
+
+ private static void recursiveDeleteSecure(SecureDirectoryStream sds) {
+ for (Path p : sds) {
+ Path file = p.getFileName();
+ BasicFileAttributes attrs;
+ try {
+ attrs = sds.getFileAttributeView(file, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS)
+ .readAttributes();
+ } catch (IOException e) {
+ log.debugf(e, "Unable to query file type of %s", p);
+ continue;
+ }
+ if (attrs.isDirectory()) {
+ try {
+ try (SecureDirectoryStream nested = sds.newDirectoryStream(file)) {
+ recursiveDeleteSecure(nested);
+ }
+ sds.deleteDirectory(file);
+ } catch (IOException e) {
+ log.debugf(e, "Unable to delete directory %s", p);
+ }
+ } else {
+ // log the whole path, not the file name
+ log.debugf("Delete file %s", p);
+ try {
+ sds.deleteFile(file);
+ } catch (IOException e) {
+ log.debugf(e, "Unable to delete file %s", p);
}
}
}
@@ -163,24 +194,43 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
return target;
}
+ /**
+ * Read the contents of a file as a string.
+ *
+ * @param file the file to read (must not be {@code null})
+ * @return the file content, as a string (not {@code null})
+ * @throws IOException if an error occurs when reading the file
+ * @deprecated Use {@link Files#readString(Path, Charset)} instead.
+ */
+ @Deprecated(forRemoval = true)
public static String readFile(Path file) throws IOException {
- final char[] charBuffer = new char[DEFAULT_BUFFER_SIZE];
- int n = 0;
- final StringWriter output = new StringWriter();
- try (BufferedReader input = Files.newBufferedReader(file)) {
- while ((n = input.read(charBuffer)) != -1) {
- output.write(charBuffer, 0, n);
- }
- }
- return output.getBuffer().toString();
+ return Files.readString(file, StandardCharsets.UTF_8);
}
+ /**
+ * Copy the input stream to the given output stream.
+ * Calling this method is identical to calling {@code in.transferTo(out)}.
+ *
+ * @param out the output stream (must not be {@code null})
+ * @param in the input stream (must not be {@code null})
+ * @throws IOException if an error occurs during the copy
+ * @see InputStream#transferTo(OutputStream)
+ */
public static void copy(OutputStream out, InputStream in) throws IOException {
in.transferTo(out);
}
+ /**
+ * Write a string to a file using UTF-8 encoding.
+ * The file will be created if it does not exist, and truncated if it is not empty.
+ *
+ * @param file the file to write (must not be {@code null})
+ * @param content the string to write to the file (must not be {@code null})
+ * @throws IOException if an error occurs when writing the file
+ */
public static void writeFile(Path file, String content) throws IOException {
- Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
+ Files.writeString(file, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
}
}
diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java
index 2262c937af40e..ec06ca2cc35bd 100644
--- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java
+++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java
@@ -65,7 +65,9 @@ public void writeResponse(AsyncFile file, Type genericType, ServerRequestContext
file.endHandler(new Runnable() {
@Override
public void run() {
- file.close();
+ // we don't need to wait for the file to be closed, we just need to make sure it does get closed
+ //noinspection ResultOfMethodCallIgnored
+ file.close().subscribeAsCompletionStage();
response.end();
// Not sure if I need to resume, actually
ctx.resume();