From 2ffe01295d7fea5814f03657a29edeca770599ca Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 31 Oct 2024 13:56:39 +0100 Subject: [PATCH 1/6] Quartz: introduce Nonconcurrent - the behavior is identical to a Job class annotated with DisallowConcurrentExecution - fixes #44048 --- .../quartz/deployment/QuartzProcessor.java | 21 +++- .../test/NonconcurrentJobDefinitionTest.java | 67 ++++++++++++ .../test/NonconcurrentOnQuartzThreadTest.java | 56 ++++++++++ .../test/NonconcurrentProgrammaticTest.java | 91 ++++++++++++++++ .../quartz/test/NonconcurrentTest.java | 54 ++++++++++ .../programmatic/ProgrammaticJobsTest.java | 6 +- .../java/io/quarkus/quartz/Nonconcurrent.java | 35 ++++++ .../io/quarkus/quartz/QuartzScheduler.java | 14 +++ .../quartz/runtime/QuartzRecorder.java | 5 +- .../quartz/runtime/QuartzSchedulerImpl.java | 101 ++++++++++++++---- .../quarkus/quartz/runtime/QuartzSupport.java | 19 +++- .../java/io/quarkus/scheduler/Scheduler.java | 36 +++---- .../common/runtime/AbstractJobDefinition.java | 57 +++++----- .../programmatic/ProgrammaticJobsTest.java | 6 +- .../scheduler/runtime/CompositeScheduler.java | 8 +- .../scheduler/runtime/SimpleScheduler.java | 4 +- 16 files changed, 499 insertions(+), 81 deletions(-) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java create mode 100644 extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 81e2739284704..9c6dfe0f27814 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -5,6 +5,7 @@ import java.sql.Connection; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -55,6 +56,7 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.quartz.Nonconcurrent; import io.quarkus.quartz.runtime.QuarkusQuartzConnectionPoolProvider; import io.quarkus.quartz.runtime.QuartzBuildTimeConfig; import io.quarkus.quartz.runtime.QuartzExtensionPointConfig; @@ -69,6 +71,7 @@ import io.quarkus.quartz.runtime.jdbc.QuarkusStdJDBCDelegate; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.deployment.ScheduledBusinessMethodItem; import io.quarkus.scheduler.deployment.SchedulerImplementationBuildItem; public class QuartzProcessor { @@ -79,6 +82,7 @@ public class QuartzProcessor { private static final DotName DELEGATE_HSQLDB = DotName.createSimple(QuarkusHSQLDBDelegate.class.getName()); private static final DotName DELEGATE_MSSQL = DotName.createSimple(QuarkusMSSQLDelegate.class.getName()); private static final DotName DELEGATE_STDJDBC = DotName.createSimple(QuarkusStdJDBCDelegate.class.getName()); + private static final DotName NONCONCURRENT = DotName.createSimple(Nonconcurrent.class); @BuildStep FeatureBuildItem feature() { @@ -313,12 +317,23 @@ public void start(BuildProducer serviceStart, @Record(RUNTIME_INIT) public void quartzSupportBean(QuartzRuntimeConfig runtimeConfig, QuartzBuildTimeConfig buildTimeConfig, QuartzRecorder recorder, - BuildProducer syntheticBeanBuildItemBuildProducer, - QuartzJDBCDriverDialectBuildItem driverDialect) { + QuartzJDBCDriverDialectBuildItem driverDialect, + List scheduledMethods, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + Set nonconcurrentMethods = new HashSet<>(); + for (ScheduledBusinessMethodItem m : scheduledMethods) { + if (m.getMethod().hasAnnotation(NONCONCURRENT)) { + nonconcurrentMethods.add(m.getMethod().declaringClass().name() + "#" + m.getMethod().name()); + } + } syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(QuartzSupport.class) .scope(Singleton.class) // this should be @ApplicationScoped but it fails for some reason .setRuntimeInit() - .supplier(recorder.quartzSupportSupplier(runtimeConfig, buildTimeConfig, driverDialect.getDriver())).done()); + .supplier(recorder.quartzSupportSupplier(runtimeConfig, buildTimeConfig, driverDialect.getDriver(), + nonconcurrentMethods)) + .done()); } + } diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java new file mode 100644 index 0000000000000..6cb0446db3528 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentJobDefinitionTest.java @@ -0,0 +1,67 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentJobDefinitionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)) + .overrideConfigKey("quarkus.scheduler.start-mode", "forced"); + + @Inject + QuartzScheduler scheduler; + + @Test + public void testExecution() throws InterruptedException { + scheduler.newJob("foo") + .setTask(se -> { + Jobs.NONCONCURRENT_COUNTER.incrementAndGet(); + try { + if (!Jobs.CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (Jobs.NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + Jobs.NONCONCURRENT_LATCH.countDown(); + } + }) + .setInterval("1s") + .setNonconcurrent() + .schedule(); + + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java new file mode 100644 index 0000000000000..dee4f50683f48 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentOnQuartzThreadTest.java @@ -0,0 +1,56 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentOnQuartzThreadTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)) + .overrideConfigKey("quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread", + "true"); + + @Test + public void testExecution() throws InterruptedException { + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Nonconcurrent + @Scheduled(identity = "foo", every = "1s") + void nonconcurrent() throws InterruptedException { + NONCONCURRENT_COUNTER.incrementAndGet(); + if (!CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + if (NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + NONCONCURRENT_LATCH.countDown(); + } + } + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java new file mode 100644 index 0000000000000..5ebeb934053c7 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentProgrammaticTest.java @@ -0,0 +1,91 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentProgrammaticTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Jobs.class)) + .overrideConfigKey("quarkus.scheduler.start-mode", "halted"); + + @Inject + QuartzScheduler scheduler; + + @Test + public void testExecution() throws SchedulerException, InterruptedException { + JobDetail job = JobBuilder.newJob(Jobs.class) + .withIdentity("foo", Scheduler.class.getName()) + .build(); + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity("foo", Scheduler.class.getName()) + .startNow() + .withSchedule(SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(1) + .repeatForever()) + .build(); + scheduler.getScheduler().scheduleJob(job, trigger); + + scheduler.resume(); + + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + @DisallowConcurrentExecution + static class Jobs implements Job { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + Jobs.NONCONCURRENT_COUNTER.incrementAndGet(); + try { + if (!Jobs.CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + if (Jobs.NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + Jobs.NONCONCURRENT_LATCH.countDown(); + } + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java new file mode 100644 index 0000000000000..ec26ba9be0524 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/NonconcurrentTest.java @@ -0,0 +1,54 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class NonconcurrentTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root.addClasses(Jobs.class)); + + @Test + public void testExecution() throws InterruptedException { + assertTrue(Jobs.NONCONCURRENT_LATCH.await(10, TimeUnit.SECONDS), + String.format("nonconcurrent() executed: %sx", Jobs.NONCONCURRENT_COUNTER.get())); + } + + static class Jobs { + + static final CountDownLatch NONCONCURRENT_LATCH = new CountDownLatch(1); + static final CountDownLatch CONCURRENT_LATCH = new CountDownLatch(5); + + static final AtomicInteger NONCONCURRENT_COUNTER = new AtomicInteger(0); + + @Nonconcurrent + @Scheduled(identity = "foo", every = "1s") + void nonconcurrent() throws InterruptedException { + NONCONCURRENT_COUNTER.incrementAndGet(); + if (!CONCURRENT_LATCH.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("nonconcurrent() execution blocked too long..."); + } + if (NONCONCURRENT_COUNTER.get() == 1) { + // concurrent() executed >= 5x and nonconcurrent() 1x + NONCONCURRENT_LATCH.countDown(); + } + } + + @Scheduled(identity = "bar", every = "1s") + void concurrent() throws InterruptedException { + CONCURRENT_LATCH.countDown(); + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java index d2f5e62a5a55e..aa027694004b9 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/ProgrammaticJobsTest.java @@ -69,7 +69,7 @@ public void testJobs() throws InterruptedException { .setSkipPredicate(AlwaysSkipPredicate.class) .schedule(); - Scheduler.JobDefinition job1 = scheduler.newJob("foo") + Scheduler.JobDefinition job1 = scheduler.newJob("foo") .setInterval("1s") .setTask(ec -> { assertTrue(Arc.container().requestContext().isActive()); @@ -79,7 +79,7 @@ public void testJobs() throws InterruptedException { assertEquals("Sync task was already set", assertThrows(IllegalStateException.class, () -> job1.setAsyncTask(ec -> null)).getMessage()); - Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); + Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); assertEquals("Either sync or async task must be set", assertThrows(IllegalStateException.class, () -> job2.schedule()).getMessage()); job2.setTask(ec -> { @@ -117,7 +117,7 @@ public void testJobs() throws InterruptedException { @Test public void testAsyncJob() throws InterruptedException, SchedulerException { String identity = "fooAsync"; - JobDefinition asyncJob = scheduler.newJob(identity) + JobDefinition asyncJob = scheduler.newJob(identity) .setInterval("1s") .setAsyncTask(ec -> { assertTrue(Context.isOnEventLoopThread() && VertxContext.isOnDuplicatedContext()); diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java new file mode 100644 index 0000000000000..27bcaa2104b04 --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/Nonconcurrent.java @@ -0,0 +1,35 @@ +package io.quarkus.quartz; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.SkippedExecution; + +/** + * A scheduled method annotated with this annotation may not be executed concurrently. The behavior is identical to a + * {@link Job} class annotated with {@link DisallowConcurrentExecution}. + *

+ * If {@code quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread} is set to + * {@code false} the execution of a scheduled method is offloaded to a specific Quarkus thread pool but the triggering Quartz + * thread is blocked until the execution is finished. Therefore, make sure the Quartz thread pool is configured appropriately. + *

+ * If {@code quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread} is set to {@code true} the scheduled method is + * invoked on a thread managed by Quartz. + *

+ * Unlike with {@link Scheduled.ConcurrentExecution#SKIP} the {@link SkippedExecution} event is never fired if a method + * execution is skipped by Quartz. + * + * @see DisallowConcurrentExecution + */ +@Target(METHOD) +@Retention(RUNTIME) +public @interface Nonconcurrent { + +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java index 395a6de8369a4..60c30ab3d7292 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/QuartzScheduler.java @@ -13,4 +13,18 @@ public interface QuartzScheduler extends Scheduler { */ org.quartz.Scheduler getScheduler(); + @Override + QuartzJobDefinition newJob(String identity); + + interface QuartzJobDefinition extends JobDefinition { + + /** + * + * @return self + * @see Nonconcurrent + */ + QuartzJobDefinition setNonconcurrent(); + + } + } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java index 9a1bd26cae449..7ea820528fcd6 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRecorder.java @@ -1,6 +1,7 @@ package io.quarkus.quartz.runtime; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import io.quarkus.runtime.annotations.Recorder; @@ -9,11 +10,11 @@ public class QuartzRecorder { public Supplier quartzSupportSupplier(QuartzRuntimeConfig runtimeConfig, - QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect) { + QuartzBuildTimeConfig buildTimeConfig, Optional driverDialect, Set nonconcurrentMethods) { return new Supplier() { @Override public QuartzSupport get() { - return new QuartzSupport(runtimeConfig, buildTimeConfig, driverDialect); + return new QuartzSupport(runtimeConfig, buildTimeConfig, driverDialect, nonconcurrentMethods); } }; } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index e67a8c9f41ac9..a231c29e65b61 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -13,9 +13,11 @@ import java.util.Properties; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -40,6 +42,7 @@ import org.jboss.logging.Logger; import org.quartz.CronScheduleBuilder; +import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDetail; @@ -60,6 +63,7 @@ import org.quartz.spi.TriggerFiredBundle; import io.quarkus.arc.Subclass; +import io.quarkus.quartz.Nonconcurrent; import io.quarkus.quartz.QuartzScheduler; import io.quarkus.runtime.StartupEvent; import io.quarkus.scheduler.DelayedExecution; @@ -223,7 +227,8 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { invoker.isBlocking() && runtimeConfig.runBlockingScheduledMethodOnQuartzThread, SchedulerUtils.parseExecutionMaxDelayAsMillis(scheduled), blockingExecutor); - JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName()); + JobDetail jobDetail = createJobBuilder(identity, method.getInvokerClassName(), + quartzSupport.isNonconcurrent(method)).build(); Optional> triggerBuilder = createTrigger(identity, scheduled, runtimeConfig, jobDetail); @@ -471,7 +476,7 @@ public Trigger getScheduledJob(String identity) { } @Override - public JobDefinition newJob(String identity) { + public QuartzJobDefinition newJob(String identity) { if (!isStarted()) { throw notStarted(); } @@ -479,7 +484,7 @@ public JobDefinition newJob(String identity) { if (scheduledTasks.containsKey(identity)) { throw new IllegalStateException("A job with this identity is already scheduled: " + identity); } - return new QuartzJobDefinition(identity); + return new QuartzJobDefinitionImpl(identity); } @Override @@ -582,13 +587,15 @@ private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSuppo props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false"); props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz); + // The org.quartz.jobStore.misfireThreshold can be used for all supported job stores + props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", + "" + runtimeConfig.misfireThreshold.toMillis()); + if (buildTimeConfig.storeType.isDbStore()) { String dataSource = buildTimeConfig.dataSourceName.orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE"); QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource); boolean serializeJobData = buildTimeConfig.serializeJobData.orElse(false); props.put(StdSchedulerFactory.PROP_JOB_STORE_USE_PROP, serializeJobData ? "false" : "true"); - props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold", - "" + runtimeConfig.misfireThreshold.toMillis()); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", buildTimeConfig.tablePrefix); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource); props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass", @@ -687,13 +694,15 @@ StartMode initStartMode(SchedulerRuntimeConfig schedulerRuntimeConfig, QuartzRun } } - private JobDetail createJobDetail(String identity, String invokerClassName) { - return JobBuilder.newJob(InvokerJob.class) + private JobBuilder createJobBuilder(String identity, String invokerClassName, boolean noncurrent) { + Class jobClass = noncurrent ? NonconcurrentInvokerJob.class + : InvokerJob.class; + return JobBuilder.newJob(jobClass) // new JobKey(identity, "io.quarkus.scheduler.Scheduler") .withIdentity(identity, Scheduler.class.getName()) // this info is redundant but keep it for backward compatibility .usingJobData(INVOKER_KEY, invokerClassName) - .requestRecovery().build(); + .requestRecovery(); } /** @@ -815,12 +824,26 @@ private Optional> createTrigger(String identity, Scheduled sch return Optional.of(triggerBuilder); } - class QuartzJobDefinition extends AbstractJobDefinition implements ExecutionMetadata { + class QuartzJobDefinitionImpl extends AbstractJobDefinition + implements ExecutionMetadata, QuartzJobDefinition { + + private boolean nonconcurrent; - QuartzJobDefinition(String id) { + QuartzJobDefinitionImpl(String id) { super(id); } + @Override + public QuartzJobDefinition setNonconcurrent() { + nonconcurrent = true; + return self(); + } + + @Override + public boolean nonconcurrent() { + return nonconcurrent; + } + @Override public boolean isRunOnVirtualThread() { return runOnVirtualThread; @@ -857,7 +880,7 @@ public Class skipPredicateClass() { } @Override - public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { + public QuartzJobDefinition setSkipPredicate(SkipPredicate skipPredicate) { if (storeType.isDbStore() && skipPredicateClass == null) { throw new IllegalStateException( "A skip predicate instance cannot be scheduled programmatically if DB store type is used; register a skip predicate class instead"); @@ -866,7 +889,7 @@ public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { } @Override - public JobDefinition setTask(Consumer task, boolean runOnVirtualThread) { + public QuartzJobDefinition setTask(Consumer task, boolean runOnVirtualThread) { if (storeType.isDbStore() && taskClass == null) { throw new IllegalStateException( "A task instance cannot be scheduled programmatically if DB store type is used; register a task class instead"); @@ -875,7 +898,7 @@ public JobDefinition setTask(Consumer task, boolean runOnVir } @Override - public JobDefinition setAsyncTask(Function> asyncTask) { + public QuartzJobDefinition setAsyncTask(Function> asyncTask) { if (storeType.isDbStore() && asyncTaskClass == null) { throw new IllegalStateException( "An async task instance cannot be scheduled programmatically if DB store type is used; register an async task class instead"); @@ -912,12 +935,15 @@ interface ExecutionMetadata { SkipPredicate skipPredicate(); Class skipPredicateClass(); + + boolean nonconcurrent(); } static final String SCHEDULED_METADATA = "scheduled_metadata"; static final String EXECUTION_METADATA_TASK_CLASS = "execution_metadata_task_class"; static final String EXECUTION_METADATA_ASYNC_TASK_CLASS = "execution_metadata_async_task_class"; static final String EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD = "execution_metadata_run_on_virtual_thread"; + static final String EXECUTION_METADATA_NONCONCURRENT = "execution_metadata_nonconcurrent"; static final String EXECUTION_METADATA_SKIP_PREDICATE_CLASS = "execution_metadata_skip_predicate_class"; QuartzTrigger createJobDefinitionQuartzTrigger(ExecutionMetadata executionMetadata, SyntheticScheduled scheduled, @@ -966,11 +992,8 @@ public boolean isBlocking() { }; } - JobBuilder jobBuilder = JobBuilder.newJob(InvokerJob.class) - // new JobKey(identity, "io.quarkus.scheduler.Scheduler") - .withIdentity(scheduled.identity(), Scheduler.class.getName()) - // this info is redundant but keep it for backward compatibility - .usingJobData(INVOKER_KEY, QuartzSchedulerImpl.class.getName()); + JobBuilder jobBuilder = createJobBuilder(scheduled.identity(), QuartzSchedulerImpl.class.getName(), + executionMetadata.nonconcurrent()); if (storeType.isDbStore()) { jobBuilder.usingJobData(SCHEDULED_METADATA, scheduled.toJson()) .usingJobData(EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD, Boolean.toString(runOnVirtualThread)); @@ -1045,6 +1068,23 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { return quartzTrigger; } + /** + * @see Nonconcurrent + */ + @DisallowConcurrentExecution + static class NonconcurrentInvokerJob extends InvokerJob { + + NonconcurrentInvokerJob(QuartzTrigger trigger, Vertx vertx) { + super(trigger, vertx); + } + + @Override + boolean awaitResult() { + return true; + } + + } + /** * Although this class is not part of the public API it must not be renamed in order to preserve backward compatibility. The * name of this class can be stored in a Quartz table in the database. See https://github.com/quarkusio/quarkus/issues/29177 @@ -1060,11 +1100,24 @@ static class InvokerJob implements Job { this.vertx = vertx; } + boolean awaitResult() { + return false; + } + @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { if (trigger != null && trigger.invoker != null) { // could be null from previous runs try { - trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); + CompletionStage ret = trigger.invoker + .invoke(new QuartzScheduledExecution(trigger, jobExecutionContext)); + if (awaitResult()) { + try { + ret.toCompletableFuture().get(); + } catch (ExecutionException | CancellationException e) { + LOGGER.warnf("Unable to retrieve result for job %s: %s", + jobExecutionContext.getJobDetail().getKey().getName(), e.toString()); + } + } } catch (Exception e) { // already logged by the StatusEmitterInvoker } @@ -1190,6 +1243,9 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr // This is a job backed by a @Scheduled method or a JobDefinition return new InvokerJob(scheduledTasks.get(bundle.getJobDetail().getKey().getName()), vertx); } + if (jobClass.equals(NonconcurrentInvokerJob.class)) { + return new NonconcurrentInvokerJob(scheduledTasks.get(bundle.getJobDetail().getKey().getName()), vertx); + } if (Subclass.class.isAssignableFrom(jobClass)) { // Get the original class from an intercepted bean class jobClass = (Class) jobClass.getSuperclass(); @@ -1218,6 +1274,7 @@ static class SerializedExecutionMetadata implements ExecutionMetadata { private final Class>> asyncTaskClass; private final boolean runOnVirtualThread; private final Class skipPredicateClass; + private final boolean nonconcurrent; @SuppressWarnings("unchecked") public SerializedExecutionMetadata(JobDetail jobDetail) { @@ -1249,6 +1306,7 @@ public SerializedExecutionMetadata(JobDetail jobDetail) { } this.runOnVirtualThread = Boolean .parseBoolean(jobDetail.getJobDataMap().getString(EXECUTION_METADATA_RUN_ON_VIRTUAL_THREAD)); + this.nonconcurrent = Boolean.parseBoolean(jobDetail.getJobDataMap().getString(EXECUTION_METADATA_NONCONCURRENT)); } @Override @@ -1271,6 +1329,11 @@ public Class>> asyncTaskClass() return asyncTaskClass; } + @Override + public boolean nonconcurrent() { + return nonconcurrent; + } + @Override public boolean isRunOnVirtualThread() { return runOnVirtualThread; diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java index b343422373b78..18944bba97041 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSupport.java @@ -1,18 +1,25 @@ package io.quarkus.quartz.runtime; import java.util.Optional; +import java.util.Set; + +import io.quarkus.quartz.Nonconcurrent; +import io.quarkus.scheduler.common.runtime.ScheduledMethod; public class QuartzSupport { private final QuartzRuntimeConfig runtimeConfig; private final QuartzBuildTimeConfig buildTimeConfig; private final Optional driverDialect; + // # + private final Set nonconcurrentMethods; public QuartzSupport(QuartzRuntimeConfig runtimeConfig, QuartzBuildTimeConfig buildTimeConfig, - Optional driverDialect) { + Optional driverDialect, Set nonconcurrentMethods) { this.runtimeConfig = runtimeConfig; this.buildTimeConfig = buildTimeConfig; this.driverDialect = driverDialect; + this.nonconcurrentMethods = Set.copyOf(nonconcurrentMethods); } public QuartzRuntimeConfig getRuntimeConfig() { @@ -26,4 +33,14 @@ public QuartzBuildTimeConfig getBuildTimeConfig() { public Optional getDriverDialect() { return driverDialect; } + + /** + * + * @param method + * @return {@code true} if the scheduled method is annotated with {@link Nonconcurrent} + */ + public boolean isNonconcurrent(ScheduledMethod method) { + return nonconcurrentMethods.contains(method.getMethodDescription()); + } + } diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index d0d2467c160cc..97dbebb4014c5 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java @@ -103,7 +103,7 @@ public interface Scheduler { * @see Scheduled#identity() * @throws UnsupportedOperationException If the scheduler was not started */ - JobDefinition newJob(String identity); + JobDefinition newJob(String identity); /** * Removes the job previously added via {@link #newJob(String)}. @@ -130,7 +130,7 @@ public interface Scheduler { *

* The implementation is not thread-safe and should not be reused. */ - interface JobDefinition { + interface JobDefinition> { /** * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are @@ -142,7 +142,7 @@ interface JobDefinition { * @return self * @see Scheduled#cron() */ - JobDefinition setCron(String cron); + THIS setCron(String cron); /** * The schedule is defined either by {@link #setCron(String)} or by {@link #setInterval(String)}. If both methods are @@ -157,7 +157,7 @@ interface JobDefinition { * @return self * @see Scheduled#every() */ - JobDefinition setInterval(String every); + THIS setInterval(String every); /** * {@link Scheduled#delayed()} @@ -166,7 +166,7 @@ interface JobDefinition { * @return self * @see Scheduled#delayed() */ - JobDefinition setDelayed(String period); + THIS setDelayed(String period); /** * {@link Scheduled#concurrentExecution()} @@ -175,7 +175,7 @@ interface JobDefinition { * @return self * @see Scheduled#concurrentExecution() */ - JobDefinition setConcurrentExecution(ConcurrentExecution concurrentExecution); + THIS setConcurrentExecution(ConcurrentExecution concurrentExecution); /** * {@link Scheduled#skipExecutionIf()} @@ -184,7 +184,7 @@ interface JobDefinition { * @return self * @see Scheduled#skipExecutionIf() */ - JobDefinition setSkipPredicate(SkipPredicate skipPredicate); + THIS setSkipPredicate(SkipPredicate skipPredicate); /** * {@link Scheduled#skipExecutionIf()} @@ -193,7 +193,7 @@ interface JobDefinition { * @return self * @see Scheduled#skipExecutionIf() */ - JobDefinition setSkipPredicate(Class skipPredicateClass); + THIS setSkipPredicate(Class skipPredicateClass); /** * {@link Scheduled#overdueGracePeriod()} @@ -202,7 +202,7 @@ interface JobDefinition { * @return self * @see Scheduled#overdueGracePeriod() */ - JobDefinition setOverdueGracePeriod(String period); + THIS setOverdueGracePeriod(String period); /** * {@link Scheduled#timeZone()} @@ -210,7 +210,7 @@ interface JobDefinition { * @return self * @see Scheduled#timeZone() */ - JobDefinition setTimeZone(String timeZone); + THIS setTimeZone(String timeZone); /** * {@link Scheduled#executeWith()} @@ -220,7 +220,7 @@ interface JobDefinition { * @throws IllegalArgumentException If the composite scheduler is used and the selected implementation is not available * @see Scheduled#executeWith() */ - JobDefinition setExecuteWith(String implementation); + THIS setExecuteWith(String implementation); /** * {@link Scheduled#executionMaxDelay()} @@ -229,14 +229,14 @@ interface JobDefinition { * @return self * @see Scheduled#executionMaxDelay() */ - JobDefinition setExecutionMaxDelay(String maxDelay); + THIS setExecutionMaxDelay(String maxDelay); /** * * @param task * @return self */ - default JobDefinition setTask(Consumer task) { + default THIS setTask(Consumer task) { return setTask(task, false); } @@ -256,7 +256,7 @@ default JobDefinition setTask(Consumer task) { * @param taskClass * @return self */ - default JobDefinition setTask(Class> taskClass) { + default THIS setTask(Class> taskClass) { return setTask(taskClass, false); } @@ -267,7 +267,7 @@ default JobDefinition setTask(Class> task * @param runOnVirtualThread whether the task must be run on a virtual thread if the JVM allows it. * @return self */ - JobDefinition setTask(Consumer task, boolean runOnVirtualThread); + THIS setTask(Consumer task, boolean runOnVirtualThread); /** * The class must either represent a CDI bean or declare a public no-args constructor. @@ -286,14 +286,14 @@ default JobDefinition setTask(Class> task * @param runOnVirtualThread * @return self */ - JobDefinition setTask(Class> consumerClass, boolean runOnVirtualThread); + THIS setTask(Class> consumerClass, boolean runOnVirtualThread); /** * * @param asyncTask * @return self */ - JobDefinition setAsyncTask(Function> asyncTask); + THIS setAsyncTask(Function> asyncTask); /** * The class must either represent a CDI bean or declare a public no-args constructor. @@ -311,7 +311,7 @@ default JobDefinition setTask(Class> task * @param asyncTaskClass * @return self */ - JobDefinition setAsyncTask(Class>> asyncTaskClass); + THIS setAsyncTask(Class>> asyncTaskClass); /** * Attempts to schedule the job. diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java index d94f1c612a378..d7a391628b7dd 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java @@ -12,7 +12,7 @@ import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; import io.smallrye.mutiny.Uni; -public abstract class AbstractJobDefinition implements JobDefinition { +public abstract class AbstractJobDefinition> implements JobDefinition { protected final String identity; protected String cron = ""; @@ -37,104 +37,104 @@ public AbstractJobDefinition(String identity) { } @Override - public JobDefinition setCron(String cron) { + public THIS setCron(String cron) { checkScheduled(); this.cron = Objects.requireNonNull(cron); - return this; + return self(); } @Override - public JobDefinition setInterval(String every) { + public THIS setInterval(String every) { checkScheduled(); this.every = Objects.requireNonNull(every); - return this; + return self(); } @Override - public JobDefinition setDelayed(String period) { + public THIS setDelayed(String period) { checkScheduled(); this.delayed = Objects.requireNonNull(period); - return this; + return self(); } @Override - public JobDefinition setConcurrentExecution(ConcurrentExecution concurrentExecution) { + public THIS setConcurrentExecution(ConcurrentExecution concurrentExecution) { checkScheduled(); this.concurrentExecution = Objects.requireNonNull(concurrentExecution); - return this; + return self(); } @Override - public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { + public THIS setSkipPredicate(SkipPredicate skipPredicate) { checkScheduled(); this.skipPredicate = Objects.requireNonNull(skipPredicate); - return this; + return self(); } @Override - public JobDefinition setSkipPredicate(Class skipPredicateClass) { + public THIS setSkipPredicate(Class skipPredicateClass) { checkScheduled(); this.skipPredicateClass = Objects.requireNonNull(skipPredicateClass); return setSkipPredicate(SchedulerUtils.instantiateBeanOrClass(skipPredicateClass)); } @Override - public JobDefinition setOverdueGracePeriod(String period) { + public THIS setOverdueGracePeriod(String period) { checkScheduled(); this.overdueGracePeriod = Objects.requireNonNull(period); - return this; + return self(); } @Override - public JobDefinition setTimeZone(String timeZone) { + public THIS setTimeZone(String timeZone) { checkScheduled(); this.timeZone = Objects.requireNonNull(timeZone); - return this; + return self(); } @Override - public JobDefinition setExecuteWith(String implementation) { + public THIS setExecuteWith(String implementation) { checkScheduled(); this.implementation = Objects.requireNonNull(implementation); - return this; + return self(); } @Override - public JobDefinition setExecutionMaxDelay(String maxDelay) { + public THIS setExecutionMaxDelay(String maxDelay) { checkScheduled(); this.executionMaxDelay = maxDelay; - return this; + return self(); } @Override - public JobDefinition setTask(Consumer task, boolean runOnVirtualThread) { + public THIS setTask(Consumer task, boolean runOnVirtualThread) { checkScheduled(); if (asyncTask != null) { throw new IllegalStateException("Async task was already set"); } this.task = Objects.requireNonNull(task); this.runOnVirtualThread = runOnVirtualThread; - return this; + return self(); } @Override - public JobDefinition setTask(Class> taskClass, boolean runOnVirtualThread) { + public THIS setTask(Class> taskClass, boolean runOnVirtualThread) { this.taskClass = Objects.requireNonNull(taskClass); return setTask(SchedulerUtils.instantiateBeanOrClass(taskClass), runOnVirtualThread); } @Override - public JobDefinition setAsyncTask(Function> asyncTask) { + public THIS setAsyncTask(Function> asyncTask) { checkScheduled(); if (task != null) { throw new IllegalStateException("Sync task was already set"); } this.asyncTask = Objects.requireNonNull(asyncTask); - return this; + return self(); } @Override - public JobDefinition setAsyncTask(Class>> asyncTaskClass) { + public THIS setAsyncTask(Class>> asyncTaskClass) { this.asyncTaskClass = Objects.requireNonNull(asyncTaskClass); return setAsyncTask(SchedulerUtils.instantiateBeanOrClass(asyncTaskClass)); } @@ -145,4 +145,9 @@ protected void checkScheduled() { } } + @SuppressWarnings("unchecked") + protected THIS self() { + return (THIS) this; + } + } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java index 3bd4446a7a44f..cd8910628bd53 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/programmatic/ProgrammaticJobsTest.java @@ -63,7 +63,7 @@ public void testJobs() throws InterruptedException { .setSkipPredicate(AlwaysSkipPredicate.class) .schedule(); - Scheduler.JobDefinition job1 = scheduler.newJob("foo") + Scheduler.JobDefinition job1 = scheduler.newJob("foo") .setInterval("1s") .setTask(ec -> { assertTrue(Arc.container().requestContext().isActive()); @@ -73,7 +73,7 @@ public void testJobs() throws InterruptedException { assertEquals("Sync task was already set", assertThrows(IllegalStateException.class, () -> job1.setAsyncTask(ec -> null)).getMessage()); - Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); + Scheduler.JobDefinition job2 = scheduler.newJob("foo").setCron("0/5 * * * * ?"); assertEquals("Either sync or async task must be set", assertThrows(IllegalStateException.class, () -> job2.schedule()).getMessage()); job2.setTask(ec -> { @@ -110,7 +110,7 @@ public void testJobs() throws InterruptedException { @Test public void testAsyncJob() throws InterruptedException { - JobDefinition asyncJob = scheduler.newJob("fooAsync") + JobDefinition asyncJob = scheduler.newJob("fooAsync") .setInterval("1s") .setAsyncTask(ec -> { assertTrue(Context.isOnEventLoopThread() && VertxContext.isOnDuplicatedContext()); diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java index 3832f3a1fb436..c41f17a47f9f0 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java @@ -113,7 +113,7 @@ public Trigger getScheduledJob(String identity) { } @Override - public JobDefinition newJob(String identity) { + public CompositeJobDefinition newJob(String identity) { return new CompositeJobDefinition(identity); } @@ -133,14 +133,14 @@ public String implementation() { return Scheduled.AUTO; } - class CompositeJobDefinition extends AbstractJobDefinition { + public class CompositeJobDefinition extends AbstractJobDefinition { public CompositeJobDefinition(String identity) { super(identity); } @Override - public JobDefinition setExecuteWith(String implementation) { + public CompositeJobDefinition setExecuteWith(String implementation) { Objects.requireNonNull(implementation); if (!Scheduled.AUTO.equals(implementation)) { if (schedulers.stream().map(Scheduler::implementation).noneMatch(implementation::equals)) { @@ -164,7 +164,7 @@ public Trigger schedule() { throw new IllegalStateException("Matching scheduler implementation not found: " + implementation); } - private JobDefinition copy(JobDefinition to) { + private JobDefinition copy(JobDefinition to) { to.setCron(cron); to.setInterval(every); to.setDelayed(delayed); 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 595a23e9404aa..50950d516e006 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 @@ -174,7 +174,7 @@ public String implementation() { } @Override - public JobDefinition newJob(String identity) { + public SimpleJobDefinition newJob(String identity) { if (!isStarted()) { throw notStarted(); } @@ -603,7 +603,7 @@ public Instant getScheduledFireTime() { } - class SimpleJobDefinition extends AbstractJobDefinition { + public class SimpleJobDefinition extends AbstractJobDefinition { private final SchedulerConfig schedulerConfig; From bd03e6cdc8936cddd588bb262ed1c7fcb8a230de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Sun, 10 Nov 2024 12:57:36 +0100 Subject: [PATCH 2/6] Include allowed roles in security scheme scopes in OpenApi 3.1+ --- .../deployment/SmallRyeOpenApiProcessor.java | 7 +- .../deployment/filter/OperationFilter.java | 6 +- .../AutoSecurityRolesAllowedTestCase.java | 22 ++- ...RolesAllowedUnsupportedScopesTestCase.java | 183 ++++++++++++++++++ ...rityRolesAllowedWithInterfaceTestCase.java | 8 +- 5 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 2345f482c5f80..3fc9752fd8f91 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -560,7 +560,7 @@ private OASFilter getOperationFilter(OpenApiFilteredIndexViewBuildItem indexView if (!classNamesMethods.isEmpty() || !rolesAllowedMethods.isEmpty() || !authenticatedMethods.isEmpty()) { return new OperationFilter(classNamesMethods, rolesAllowedMethods, authenticatedMethods, config.securitySchemeName, - config.autoAddTags, config.autoAddOperationSummary); + config.autoAddTags, config.autoAddOperationSummary, isOpenApi_3_1_0_OrGreater(config)); } return null; @@ -1169,4 +1169,9 @@ private List getResourceFiles(Path resourcePath, Path target) { } return filenames; } + + private static boolean isOpenApi_3_1_0_OrGreater(SmallRyeOpenApiConfig config) { + final String openApiVersion = config.openApiVersion.orElse(null); + return openApiVersion == null || (!openApiVersion.startsWith("2") && !openApiVersion.startsWith("3.0")); + } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java index f33c0e56460bd..640a919febf5d 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java @@ -42,12 +42,13 @@ public class OperationFilter implements OASFilter { private final String defaultSecuritySchemeName; private final boolean doAutoTag; private final boolean doAutoOperation; + private final boolean alwaysIncludeScopesValidForScheme; public OperationFilter(Map classNameMap, Map> rolesAllowedMethodReferences, List authenticatedMethodReferences, String defaultSecuritySchemeName, - boolean doAutoTag, boolean doAutoOperation) { + boolean doAutoTag, boolean doAutoOperation, boolean alwaysIncludeScopesValidForScheme) { this.classNameMap = Objects.requireNonNull(classNameMap); this.rolesAllowedMethodReferences = Objects.requireNonNull(rolesAllowedMethodReferences); @@ -55,13 +56,14 @@ public OperationFilter(Map classNameMap, this.defaultSecuritySchemeName = Objects.requireNonNull(defaultSecuritySchemeName); this.doAutoTag = doAutoTag; this.doAutoOperation = doAutoOperation; + this.alwaysIncludeScopesValidForScheme = alwaysIncludeScopesValidForScheme; } @Override public void filterOpenAPI(OpenAPI openAPI) { var securityScheme = getSecurityScheme(openAPI); String schemeName = securityScheme.map(Map.Entry::getKey).orElse(defaultSecuritySchemeName); - boolean scopesValidForScheme = securityScheme.map(Map.Entry::getValue) + boolean scopesValidForScheme = alwaysIncludeScopesValidForScheme || securityScheme.map(Map.Entry::getValue) .map(SecurityScheme::getType) .map(Set.of(SecurityScheme.Type.OAUTH2, SecurityScheme.Type.OPENIDCONNECT)::contains) .orElse(false); diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java index 7a95fe1aaf5f3..ecbd54b7f603b 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java @@ -2,6 +2,7 @@ import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; @@ -65,20 +66,20 @@ void testAutoSecurityRequirement() { not(hasKey("my-extension2")))) .and() // OpenApiResourceSecuredAtMethodLevel - .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurityScheme("admin")) .body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurityScheme("user1")) + .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurityScheme("user2")) .body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue()) .body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurityScheme("admin")) .body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity) .and() // OpenApiResourceSecuredAtClassLevel - .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity) - .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurityScheme("user1")) + .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurityScheme("user2")) .body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName")) - .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurityScheme("admin")) .and() // OpenApiResourceSecuredAtMethodLevel2 .body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel")) @@ -173,4 +174,11 @@ void testOpenAPIAnnotations() { Matchers.equalTo("Not Allowed")); } + static Matcher> defaultSecurityScheme(String... roles) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo("JWTCompanyAuthentication"), containsInAnyOrder(roles))))); + } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java new file mode 100644 index 0000000000000..fd7f6b9244352 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java @@ -0,0 +1,183 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Run same tests as {@link AutoSecurityRolesAllowedTestCase}, but with OpenAPI version 3.0.2 + * that only allowed security requirement scopes for Oauth2 and OpenID Connect schemes. + */ +public class AutoSecurityRolesAllowedUnsupportedScopesTestCase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ResourceBean.class, OpenApiResourceSecuredAtClassLevel.class, + OpenApiResourceSecuredAtClassLevel2.class, OpenApiResourceSecuredAtMethodLevel.class, + OpenApiResourceSecuredAtMethodLevel2.class) + .addAsResource( + new StringAsset(""" + quarkus.smallrye-openapi.open-api-version=3.0.2 + quarkus.smallrye-openapi.security-scheme=jwt + quarkus.smallrye-openapi.security-scheme-name=JWTCompanyAuthentication + quarkus.smallrye-openapi.security-scheme-description=JWT Authentication + quarkus.smallrye-openapi.security-scheme-extensions.x-my-extension1=extension-value + quarkus.smallrye-openapi.security-scheme-extensions.my-extension2=extension-value + """), + "application.properties")); + + static Matcher> schemeArray(String schemeName) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo(schemeName), emptyIterable())))); + } + + @Test + void testAutoSecurityRequirement() { + var defaultSecurity = schemeArray("JWTCompanyAuthentication"); + + RestAssured.given() + .header("Accept", "application/json") + .when() + .get("/q/openapi") + .then() + .log().body() + .and() + .body("openapi", Matchers.is("3.0.2")) + .body("components.securitySchemes.JWTCompanyAuthentication", allOf( + hasEntry("type", "http"), + hasEntry("scheme", "bearer"), + hasEntry("bearerFormat", "JWT"), + hasEntry("description", "JWT Authentication"), + hasEntry("x-my-extension1", "extension-value"), + not(hasKey("my-extension2")))) + .and() + // OpenApiResourceSecuredAtMethodLevel + .body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue()) + .body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity) + .and() + // OpenApiResourceSecuredAtClassLevel + .body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity) + .body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName")) + .body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity) + .and() + // OpenApiResourceSecuredAtMethodLevel2 + .body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel")) + .and() + // OpenApiResourceSecuredAtClassLevel2 + .body("paths.'/resource3/test-security/classLevel-2/1'.get.security", defaultSecurity); + } + + @Test + void testOpenAPIAnnotations() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then() + .log().body() + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/classLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/3'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/classLevel/4'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/naked'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/2'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.401.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/methodLevel/public'.get.responses.403.description", + Matchers.nullValue()) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/annotated/documented'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.401.description", + Matchers.equalTo("Who are you?")) + .and() + .body("paths.'/resource2/test-security/methodLevel/3'.get.responses.403.description", + Matchers.equalTo("You cannot do that.")) + .and() + .body("paths.'/resource2/test-security/methodLevel/4'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource2/test-security/methodLevel/4'.get.responses.403.description", + Matchers.equalTo("Not Allowed")) + .and() + .body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.401.description", + Matchers.equalTo("Not Authorized")) + .and() + .body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.403.description", + Matchers.equalTo("Not Allowed")); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java index 086456e1f0757..c2063d53f6c88 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java @@ -2,7 +2,7 @@ import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalToObject; import static org.hamcrest.Matchers.hasEntry; @@ -24,18 +24,18 @@ class AutoSecurityRolesAllowedWithInterfaceTestCase { .addClasses(ApplicationContext.class, FooAPI.class, FooResource.class)); - static Matcher> schemeArray(String schemeName) { + static Matcher> schemeArray(String schemeName, String... roles) { return allOf( iterableWithSize(1), hasItem(allOf( aMapWithSize(1), - hasEntry(equalTo(schemeName), emptyIterable())))); + hasEntry(equalTo(schemeName), containsInAnyOrder(roles))))); } @Test void testAutoSecurityRequirement() { - var oidcAuth = schemeArray("oidc_auth"); + var oidcAuth = schemeArray("oidc_auth", "RoleXY"); RestAssured.given() .header("Accept", "application/json") From 686cc7d2ff00dfb69ea21ce98893b202851a3de0 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Wed, 6 Nov 2024 16:28:07 +0100 Subject: [PATCH 3/6] Propose ADR for restructuring Quarkus workshops organization - Suggests moving from a single repository to individual repositories per workshop - Aims to simplify management, improve discoverability, and enable GitHub Pages for documentation - Addresses limitations of current structure in hosting and maintaining multiple workshops --- adr/0007-quarkus-workshop-structure.adoc | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 adr/0007-quarkus-workshop-structure.adoc diff --git a/adr/0007-quarkus-workshop-structure.adoc b/adr/0007-quarkus-workshop-structure.adoc new file mode 100644 index 0000000000000..9bfd794156568 --- /dev/null +++ b/adr/0007-quarkus-workshop-structure.adoc @@ -0,0 +1,81 @@ += Structure for Quarkus Workshops + +* Status: _Accepted_ +* Date: 2024-11-06 +* Authors: @cescoffier + +== Context and Problem Statement + +Since the public release of Quarkus, we launched a hands-on workshop to help developers get started with it. +Known as the "Quarkus Superheroes" workshop, this workshop allowed developers to learn Quarkus by actively writing and running code in a structured environment, often at conferences or in classroom settings. + +The "Quarkus Superheroes" workshop has been highly successful, delivered at various conferences and widely used by developers for self-study. +At the time, we anticipated additional workshops, leading us to establish a dedicated structure within a single repository: Quarkus Workshops (https://github.com/quarkusio/quarkus-workshops). + +The initial (and still existing) structure was straightforward: + +[source] +---- +. +├── README.md +└── quarkus-workshop-super-heroes/ + ├── dist + ├── docs + ├── super-heroes + ├── README.adoc + └── pom.xml +---- + +Although this structure was meant to support multiple workshops, only the "Quarkus Superheroes" workshop was added. +Instead of separate workshops, we expanded this initial workshop with additional steps and features. + +As we now develop new workshops on various topics, we face limitations with the single repository structure. +For example, the Quarkus LangChain4J workshop was created separately to demonstrate Quarkus LangChain4J usage, yet it isn’t integrated into the main workshop repository. +Additionally, having a single repository complicates using GitHub Pages for documentation. + +Given the current and future workshops, it’s essential to reconsider the structure to allow easier management and discoverability of each workshop. + +== Proposed New Structure + +Our experience shows that hosting all workshops in one repository isn’t optimal. We propose a new structure as follows: + +1. Each workshop will be hosted in its own repository. +This simplifies management, avoids conflicts in `README` and documentation setup, and improves workshop discoverability. +2. Naming convention: Each workshop repository should follow the format `quarkus-workshop-`, where `` represents the workshop subject (e.g., `quarkus-workshop-superheroes`, `quarkus-workshop-langchain4j`). +3. Documentation should be hosted with GitHub Pages in each repository, making each workshop more accessible. +4. Each workshop repository should have the `workshop` topic to facilitate discoverability. +5. We will keep https://quarkus.io/quarkus-workshops/ as a landing page, which people can use to find workshops. +In order to preserve the GitHub history, the quarkus-workshops repository should be renamed to https://quarkus.io/quarkus-workshop-superheroes, and then a new repository should be created, using the old name, `quarkus-workshops`. +6. This _landing_ repository can also be used to host redirects. For example, the existing URL https://quarkus.io/quarkus-workshops/super-heroes/ should be kept valid by using a redirect. + +== Considered Options + +=== Option 1: Continue with the current single-repository approach + +This would mean keeping all workshops under the existing repository. +However, as observed, this approach has not met expectations and makes workshop management more challenging. + +=== Option 2: Create a separate organization for workshops + +A dedicated organization could host all workshops, offering a single access point. +However, this approach could reduce discoverability, but would not use the Quarkus organization’s CI resources. +CI resource usage is minor, as workshops are not frequently updated. + +== Consequences + +=== Positive + +* Simplified workshop management. +* Greater autonomy for workshop maintainers. +* Consolidation of workshops previously hosted in separate repositories. + +=== Negative + +* Lack of a central place to list all workshops. This could be mitigated by creating a dedicated page on the Quarkus website. +* Potential CI resource shortage as each workshop repository uses _quarkusio_ organization CI resources. +However, this is unlikely to be a significant issue, as, generally, workshops don't use much CI resources. +That being said, it would require monitoring to ensure it doesn't become a problem. + +=== Neutral + +* Existing workshops would need restructuring to align with the new approach, especially the Quarkus Superheroes workshop. From 95fea0b5da08c9ec968bfb8e3b42c621ce4412cb Mon Sep 17 00:00:00 2001 From: Ales Justin Date: Thu, 17 Oct 2024 12:50:53 +0200 Subject: [PATCH 4/6] Add SimpleSpanProcessor support --- .../_includes/opentelemetry-config.adoc | 8 +++ .../deployment/OpenTelemetryProcessor.java | 1 + .../exporter/otlp/OtlpExporterProcessor.java | 11 +++-- .../OpenTelemetryDisabledSdkTest.java | 6 +-- .../otlp/OtlpTraceExporterDisabledTest.java | 6 +-- .../OpenTelemetrySamplerConfigTest.java | 4 +- ...uredOpenTelemetrySdkBuilderCustomizer.java | 49 ++++++++++++++++++- .../runtime/config/build/OTelBuildConfig.java | 11 +++++ .../exporter/otlp/OTelExporterRecorder.java | 39 +++++++++------ ...essor.java => LateBoundSpanProcessor.java} | 15 +++--- ...RemoveableLateBoundBatchSpanProcessor.java | 16 ------ .../RemoveableLateBoundSpanProcessor.java | 16 ++++++ .../vertx/exporter/SimpleProfile.java | 12 +++++ .../SimpleGrpcNoTLSNoCompressionTest.java | 15 ++++++ .../SimpleGrpcNoTLSWithCompressionTest.java | 16 ++++++ .../SimpleGrpcWithTLSNoCompressionTest.java | 16 ++++++ .../SimpleGrpcWithTLSWithCompressionTest.java | 19 +++++++ ...ithTLSWithTrustAllWithCompressionTest.java | 32 ++++++++++++ .../SimpleHttpNoTLSNoCompressionTest.java | 16 ++++++ .../SimpleHttpNoTLSWithCompressionTest.java | 19 +++++++ .../SimpleHttpWithTLSNoCompressionTest.java | 19 +++++++ .../SimpleHttpWithTLSWithCompressionTest.java | 20 ++++++++ ...thTLSWithCompressionUsingRegistryTest.java | 21 ++++++++ ...ithTLSWithTrustAllWithCompressionTest.java | 33 +++++++++++++ 24 files changed, 368 insertions(+), 52 deletions(-) rename extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/{LateBoundBatchSpanProcessor.java => LateBoundSpanProcessor.java} (84%) delete mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java create mode 100644 extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java create mode 100644 integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java diff --git a/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc b/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc index cf4dcfff15f79..8eb3b1ba481e2 100644 --- a/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc +++ b/docs/src/main/asciidoc/_includes/opentelemetry-config.adoc @@ -34,3 +34,11 @@ quarkus.otel.exporter.otlp.logs.endpoint=http://logs-uri:4317 // <3> <1> The endpoint for the traces exporter. <2> The endpoint for the metrics exporter. <3> The endpoint for the logs exporter. + +If you need that your spans and logs to be exported directly as they finish +(e.g. in a serverless environment / application), you can set this property to `true`. +This replaces the default batching of data. +[source,properties] +---- +quarkus.otel.simple=true +---- diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 5d34b1c7c4e69..4b1846f90a857 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -96,6 +96,7 @@ AdditionalBeanBuildItem ensureProducerIsRetained() { return AdditionalBeanBuildItem.builder() .setUnremovable() .addBeanClasses( + AutoConfiguredOpenTelemetrySdkBuilderCustomizer.SimpleLogRecordProcessorCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracingResourceCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.SamplerCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer.class, diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java index e33d6271665b4..6fc49278641f0 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java @@ -28,7 +28,7 @@ import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfigBuilder; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; import io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterRecorder; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; @@ -84,7 +84,8 @@ void config(BuildProducer runTimeConfigBuilderPro @BuildStep(onlyIf = OtlpExporterProcessor.OtlpTracingExporterEnabled.class) @Record(ExecutionTime.RUNTIME_INIT) @Consume(TlsRegistryBuildItem.class) - void createBatchSpanProcessor(OTelExporterRecorder recorder, + void createSpanProcessor(OTelExporterRecorder recorder, + OTelBuildConfig oTelBuildConfig, OTelRuntimeConfig otelRuntimeConfig, OtlpExporterRuntimeConfig exporterRuntimeConfig, CoreVertxBuildItem vertxBuildItem, @@ -95,7 +96,7 @@ void createBatchSpanProcessor(OTelExporterRecorder recorder, return; } syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem - .configure(LateBoundBatchSpanProcessor.class) + .configure(LateBoundSpanProcessor.class) .types(SpanProcessor.class) .setRuntimeInit() .scope(Singleton.class) @@ -103,8 +104,8 @@ void createBatchSpanProcessor(OTelExporterRecorder recorder, .addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), new Type[] { ClassType.create(DotName.createSimple(SpanExporter.class.getName())) }, null)) .addInjectionPoint(ClassType.create(DotName.createSimple(TlsConfigurationRegistry.class))) - .createWith(recorder.batchSpanProcessorForOtlp(otelRuntimeConfig, exporterRuntimeConfig, - vertxBuildItem.getVertx())) + .createWith(recorder.spanProcessorForOtlp(oTelBuildConfig, otelRuntimeConfig, + exporterRuntimeConfig, vertxBuildItem.getVertx())) .done()); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java index a547f90bef18f..5c7faccb33257 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java @@ -12,7 +12,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.HttpInstrumenterVertxTracer; import io.quarkus.test.QuarkusUnitTest; import io.vertx.core.spi.observability.HttpRequest; @@ -26,7 +26,7 @@ public class OpenTelemetryDisabledSdkTest { .overrideConfigKey("quarkus.otel.sdk.disabled", "true"); @Inject - LateBoundBatchSpanProcessor batchSpanProcessor; + LateBoundSpanProcessor spanProcessor; @Inject OpenTelemetry openTelemetry; @@ -40,7 +40,7 @@ public class OpenTelemetryDisabledSdkTest { @Test void testNoTracer() { // The OTel API doesn't provide a clear way to check if a tracer is an effective NOOP tracer. - Assertions.assertTrue(batchSpanProcessor.isDelegateNull(), "BatchSpanProcessor delegate must not be set"); + Assertions.assertTrue(spanProcessor.isDelegateNull(), "SpanProcessor delegate must not be set"); } @Test diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java index 7d9e074025c18..1a0372f194396 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; import io.quarkus.test.QuarkusUnitTest; public class OtlpTraceExporterDisabledTest { @@ -25,7 +25,7 @@ public class OtlpTraceExporterDisabledTest { OpenTelemetry openTelemetry; @Inject - Instance lateBoundBatchSpanProcessorInstance; + Instance lateBoundSpanProcessorInstance; @Inject Instance metricExporters; @@ -33,7 +33,7 @@ public class OtlpTraceExporterDisabledTest { @Test void testOpenTelemetryButNoBatchSpanProcessor() { assertNotNull(openTelemetry); - assertFalse(lateBoundBatchSpanProcessorInstance.isResolvable()); + assertFalse(lateBoundSpanProcessorInstance.isResolvable()); assertFalse(metricExporters.isResolvable()); } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java index 86218be707ce6..f9c9e4c174772 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.lang.reflect.InvocationTargetException; +import java.util.Locale; import jakarta.inject.Inject; @@ -36,6 +37,7 @@ public class OpenTelemetrySamplerConfigTest { void test() throws NoSuchFieldException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { Sampler sampler = TestUtil.getSampler(openTelemetry); - assertEquals(String.format("TraceIdRatioBased{%.6f}", 0.5d), sampler.getDescription()); + // Fix the locale to ROOT, so we don't get 0,500000 + assertEquals(String.format(Locale.ROOT, "TraceIdRatioBased{%.6f}", 0.5d), sampler.getDescription()); } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java index e601962c7b01c..eac3ccb8c1e89 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/AutoConfiguredOpenTelemetrySdkBuilderCustomizer.java @@ -18,6 +18,10 @@ import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.IdGenerator; @@ -27,7 +31,7 @@ import io.quarkus.arc.All; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.propagation.TextMapPropagatorCustomizer; import io.quarkus.opentelemetry.runtime.tracing.DelayedAttributes; import io.quarkus.opentelemetry.runtime.tracing.DropTargetsSampler; @@ -39,6 +43,47 @@ public interface AutoConfiguredOpenTelemetrySdkBuilderCustomizer { void customize(AutoConfiguredOpenTelemetrySdkBuilder builder); + @Singleton + final class SimpleLogRecordProcessorCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { + private SimpleLogRecordProcessorBiFunction biFunction; + + public SimpleLogRecordProcessorCustomizer( + OTelBuildConfig oTelBuildConfig, + Instance ilre) { + if (oTelBuildConfig.simple() && ilre.isResolvable()) { + LogRecordProcessor lrp = SimpleLogRecordProcessor.create(ilre.get()); + this.biFunction = new SimpleLogRecordProcessorBiFunction(lrp); + } + } + + @Override + public void customize(AutoConfiguredOpenTelemetrySdkBuilder builder) { + if (biFunction != null) { + builder.addLogRecordProcessorCustomizer(biFunction); + } + } + } + + class SimpleLogRecordProcessorBiFunction + implements BiFunction { + + private final LogRecordProcessor logRecordProcessor; + + public SimpleLogRecordProcessorBiFunction(LogRecordProcessor logRecordProcessor) { + this.logRecordProcessor = logRecordProcessor; + } + + @Override + public LogRecordProcessor apply(LogRecordProcessor lrp, ConfigProperties cp) { + // only change batch lrp, leave others + if (lrp instanceof BatchLogRecordProcessor) { + return logRecordProcessor; + } else { + return lrp; + } + } + } + @Singleton final class TracingResourceCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { @@ -174,7 +219,7 @@ public SdkTracerProviderBuilder apply(SdkTracerProviderBuilder tracerProviderBui spanProcessors.stream().filter(new Predicate() { @Override public boolean test(SpanProcessor sp) { - return !(sp instanceof RemoveableLateBoundBatchSpanProcessor); + return !(sp instanceof RemoveableLateBoundSpanProcessor); } }) .forEach(tracerProviderBuilder::addSpanProcessor); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java index c724ea9d640e7..bd47ccc601ec8 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java @@ -37,6 +37,17 @@ public interface OTelBuildConfig { @WithDefault("true") boolean enabled(); + /** + * Should we use simple processor for spans and log records. + * This will disable batch processing and the exporter will send + * telemetry data right away. + * This is recommended for serverless applications. + *

+ * Defaults to false. + */ + @WithDefault("false") + boolean simple(); + /** * Trace exporter configurations. */ diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java index 65f80415778bd..57cd1f9d2253e 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java @@ -36,8 +36,12 @@ import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; +import io.quarkus.opentelemetry.runtime.config.runtime.BatchSpanProcessorConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.*; import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.NoopLogRecordExporter; @@ -48,8 +52,8 @@ import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.VertxHttpMetricsExporter; import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxGrpcSender; import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxHttpSender; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; -import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundSpanProcessor; import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxGrpcSpanExporter; import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxHttpSpanExporter; import io.quarkus.runtime.annotations.Recorder; @@ -68,23 +72,24 @@ public class OTelExporterRecorder { public static final String BASE2EXPONENTIAL_AGGREGATION_NAME = AggregationUtil .aggregationName(Aggregation.base2ExponentialBucketHistogram()); - public Function, LateBoundBatchSpanProcessor> batchSpanProcessorForOtlp( + public Function, LateBoundSpanProcessor> spanProcessorForOtlp( + OTelBuildConfig oTelBuildConfig, OTelRuntimeConfig otelRuntimeConfig, OtlpExporterRuntimeConfig exporterRuntimeConfig, Supplier vertx) { URI baseUri = getTracesUri(exporterRuntimeConfig); // do the creation and validation here in order to preserve backward compatibility return new Function<>() { @Override - public LateBoundBatchSpanProcessor apply( - SyntheticCreationalContext context) { + public LateBoundSpanProcessor apply( + SyntheticCreationalContext context) { if (otelRuntimeConfig.sdkDisabled() || baseUri == null) { - return RemoveableLateBoundBatchSpanProcessor.INSTANCE; + return RemoveableLateBoundSpanProcessor.INSTANCE; } // Only create the OtlpGrpcSpanExporter if an endpoint was set in runtime config and was properly validated at startup Instance spanExporters = context.getInjectedReference(new TypeLiteral<>() { }); if (!spanExporters.isUnsatisfied()) { - return RemoveableLateBoundBatchSpanProcessor.INSTANCE; + return RemoveableLateBoundSpanProcessor.INSTANCE; } try { @@ -93,15 +98,21 @@ public LateBoundBatchSpanProcessor apply( var spanExporter = createSpanExporter(exporterRuntimeConfig, vertx.get(), baseUri, tlsConfigurationRegistry); - BatchSpanProcessorBuilder processorBuilder = BatchSpanProcessor.builder(spanExporter); + if (oTelBuildConfig.simple()) { + SimpleSpanProcessorBuilder processorBuilder = SimpleSpanProcessor.builder(spanExporter); + return new LateBoundSpanProcessor(processorBuilder.build()); + } else { + BatchSpanProcessorBuilder processorBuilder = BatchSpanProcessor.builder(spanExporter); - processorBuilder.setScheduleDelay(otelRuntimeConfig.bsp().scheduleDelay()); - processorBuilder.setMaxQueueSize(otelRuntimeConfig.bsp().maxQueueSize()); - processorBuilder.setMaxExportBatchSize(otelRuntimeConfig.bsp().maxExportBatchSize()); - processorBuilder.setExporterTimeout(otelRuntimeConfig.bsp().exportTimeout()); - // processorBuilder.setMeterProvider() // TODO add meter provider to span processor. + BatchSpanProcessorConfig bspc = otelRuntimeConfig.bsp(); + processorBuilder.setScheduleDelay(bspc.scheduleDelay()); + processorBuilder.setMaxQueueSize(bspc.maxQueueSize()); + processorBuilder.setMaxExportBatchSize(bspc.maxExportBatchSize()); + processorBuilder.setExporterTimeout(bspc.exportTimeout()); + // processorBuilder.setMeterProvider() // TODO add meter provider to span processor. - return new LateBoundBatchSpanProcessor(processorBuilder.build()); + return new LateBoundSpanProcessor(processorBuilder.build()); + } } catch (IllegalArgumentException iae) { throw new IllegalStateException("Unable to install OTLP Exporter", iae); } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java similarity index 84% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java index 90318940e3497..94663996276c0 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundSpanProcessor.java @@ -7,20 +7,19 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; /** - * Class to facilitate a delay in when the worker thread inside {@link BatchSpanProcessor} + * Class to facilitate a delay in when the worker thread inside {@link SpanProcessor} * is started, enabling Quarkus to instantiate a {@link io.opentelemetry.api.trace.TracerProvider} - * during static initialization and set a {@link BatchSpanProcessor} delegate during runtime initialization. + * during static initialization and set a {@link SpanProcessor} delegate during runtime initialization. */ -public class LateBoundBatchSpanProcessor implements SpanProcessor { - private static final Logger log = Logger.getLogger(LateBoundBatchSpanProcessor.class); +public class LateBoundSpanProcessor implements SpanProcessor { + private static final Logger log = Logger.getLogger(LateBoundSpanProcessor.class); private boolean warningLogged = false; - private BatchSpanProcessor delegate; + private SpanProcessor delegate; - public LateBoundBatchSpanProcessor(BatchSpanProcessor delegate) { + public LateBoundSpanProcessor(SpanProcessor delegate) { this.delegate = delegate; } @@ -104,7 +103,7 @@ private void resetDelegate() { */ private void logDelegateNotFound() { if (!warningLogged) { - log.warn("No BatchSpanProcessor delegate specified, no action taken."); + log.warn("No SpanProcessor delegate specified, no action taken."); warningLogged = true; } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java deleted file mode 100644 index d8654e5ff634e..0000000000000 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; - -import io.quarkus.opentelemetry.runtime.AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer; - -/** - * The only point in having this class is to allow {@link TracerProviderCustomizer} - * to easily ignore the configured {@link LateBoundBatchSpanProcessor}. - */ -public final class RemoveableLateBoundBatchSpanProcessor extends LateBoundBatchSpanProcessor { - - public static final RemoveableLateBoundBatchSpanProcessor INSTANCE = new RemoveableLateBoundBatchSpanProcessor(); - - private RemoveableLateBoundBatchSpanProcessor() { - super(null); - } -} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java new file mode 100644 index 0000000000000..9d04b984d7d51 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundSpanProcessor.java @@ -0,0 +1,16 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; + +import io.quarkus.opentelemetry.runtime.AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer; + +/** + * The only point in having this class is to allow {@link TracerProviderCustomizer} + * to easily ignore the configured {@link LateBoundSpanProcessor}. + */ +public final class RemoveableLateBoundSpanProcessor extends LateBoundSpanProcessor { + + public static final RemoveableLateBoundSpanProcessor INSTANCE = new RemoveableLateBoundSpanProcessor(); + + private RemoveableLateBoundSpanProcessor() { + super(null); + } +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java new file mode 100644 index 0000000000000..b0ea9d04a013e --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/SimpleProfile.java @@ -0,0 +1,12 @@ +package io.quarkus.it.opentelemetry.vertx.exporter; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public final class SimpleProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.otel.simple", "true"); + } +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java new file mode 100644 index 0000000000000..0c82fae42e1ef --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSNoCompressionTest.java @@ -0,0 +1,15 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcNoTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java new file mode 100644 index 0000000000000..407fd02c01fa7 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcNoTLSWithCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "enableCompression", value = "true"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcNoTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java new file mode 100644 index 0000000000000..8c003fdacf795 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSNoCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "enableTLS", value = "true"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcWithTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java new file mode 100644 index 0000000000000..b764521146882 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleGrpcWithTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java new file mode 100644 index 0000000000000..45ce4303120f3 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/grpc/SimpleGrpcWithTLSWithTrustAllWithCompressionTest.java @@ -0,0 +1,32 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.grpc; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(SimpleGrpcWithTLSWithTrustAllWithCompressionTest.Profile.class) +public class SimpleGrpcWithTLSWithTrustAllWithCompressionTest extends AbstractExporterTest { + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.tls.trust-all", "true", "quarkus.otel.simple", "true"); + } + + @Override + public List testResources() { + return Collections.singletonList( + new TestResourceEntry( + OtelCollectorLifecycleManager.class, + Map.of("enableTLS", "true", "enableCompression", "true", "preventTrustCert", "true"))); + } + } + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java new file mode 100644 index 0000000000000..f623ed071d58b --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSNoCompressionTest.java @@ -0,0 +1,16 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = @ResourceArg(name = "protocol", value = "http/protobuf"), restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpNoTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java new file mode 100644 index 0000000000000..8fbc960b5e9be --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpNoTLSWithCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpNoTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java new file mode 100644 index 0000000000000..a5fb15970a28c --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSNoCompressionTest.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSNoCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java new file mode 100644 index 0000000000000..77188c2cf61f5 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionTest.java @@ -0,0 +1,20 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSWithCompressionTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java new file mode 100644 index 0000000000000..31a896345339f --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithCompressionUsingRegistryTest.java @@ -0,0 +1,21 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.it.opentelemetry.vertx.exporter.SimpleProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = OtelCollectorLifecycleManager.class, initArgs = { + @ResourceArg(name = "enableTLS", value = "true"), + @ResourceArg(name = "enableCompression", value = "true"), + @ResourceArg(name = "protocol", value = "http/protobuf"), + @ResourceArg(name = "tlsRegistryName", value = "otel") +}, restrictToAnnotatedClass = true) +@TestProfile(SimpleProfile.class) +public class SimpleHttpWithTLSWithCompressionUsingRegistryTest extends AbstractExporterTest { + +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java new file mode 100644 index 0000000000000..887ab7c133df4 --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/http/SimpleHttpWithTLSWithTrustAllWithCompressionTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.opentelemetry.vertx.exporter.http; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkus.it.opentelemetry.vertx.exporter.AbstractExporterTest; +import io.quarkus.it.opentelemetry.vertx.exporter.OtelCollectorLifecycleManager; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(SimpleHttpWithTLSWithTrustAllWithCompressionTest.Profile.class) +public class SimpleHttpWithTLSWithTrustAllWithCompressionTest extends AbstractExporterTest { + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.tls.trust-all", "true", "quarkus.otel.simple", "true"); + } + + @Override + public List testResources() { + return Collections.singletonList( + new TestResourceEntry( + OtelCollectorLifecycleManager.class, + Map.of("enableTLS", "true", "enableCompression", "true", "preventTrustCert", "true", "protocol", + "http/protobuf"))); + } + } + +} From 8ce3dc5d4189ddd656e6f7394af299170e08be49 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 11 Nov 2024 14:56:20 +0100 Subject: [PATCH 5/6] QuteErrorPageSetup: support templates that are not backed by a file - fixes #44412 --- .../qute/deployment/QuteDevModeProcessor.java | 31 +++++++++++++++++++ .../runtime/devmode/QuteErrorPageSetup.java | 17 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java new file mode 100644 index 0000000000000..4baf3b0756616 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java @@ -0,0 +1,31 @@ +package io.quarkus.qute.deployment; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.qute.runtime.devmode.QuteErrorPageSetup; + +@BuildSteps(onlyIf = IsDevelopment.class) +public class QuteDevModeProcessor { + + @BuildStep + void collectGeneratedContents(List templatePaths, + BuildProducer errors) { + Map contents = new HashMap<>(); + for (TemplatePathBuildItem template : templatePaths) { + if (!template.isFileBased()) { + contents.put(template.getPath(), template.getContent()); + } + } + // Set the global that could be used at runtime when a qute error page is rendered + DevConsoleManager.setGlobal(QuteErrorPageSetup.GENERATED_CONTENTS, contents); + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java index b7ced362defac..916522c98443f 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java @@ -2,6 +2,7 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; @@ -12,6 +13,7 @@ import java.util.Comparator; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; @@ -19,6 +21,7 @@ import org.jboss.logging.Logger; import io.quarkus.dev.ErrorPageGenerators; +import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.dev.spi.HotReplacementContext; import io.quarkus.dev.spi.HotReplacementSetup; import io.quarkus.qute.Engine; @@ -33,6 +36,8 @@ public class QuteErrorPageSetup implements HotReplacementSetup { private static final Logger LOG = Logger.getLogger(QuteErrorPageSetup.class); + public static final String GENERATED_CONTENTS = "io.quarkus.qute.generatedContents"; + private static final String TEMPLATE_EXCEPTION = "io.quarkus.qute.TemplateException"; private static final String ORIGIN = "io.quarkus.qute.TemplateNode$Origin"; @@ -139,6 +144,10 @@ String getProblemInfo(int index, Throwable problem, Template problemTemplate, Es LOG.warn("Unable to read the template source: " + templateId, e); } + if (sourceLines.isEmpty()) { + return Arrays.stream(messageLines).collect(Collectors.joining("
")); + } + List realLines = new ArrayList<>(); boolean endLinesSkipped = false; if (sourceLines.size() > 15) { @@ -187,6 +196,14 @@ private BufferedReader getBufferedReader(String templateId) throws IOException { } } } + // Source file not available - try to search the generated contents + Map generatedContents = DevConsoleManager.getGlobal(GENERATED_CONTENTS); + if (generatedContents != null) { + String template = generatedContents.get(templateId); + if (template != null) { + return new BufferedReader(new StringReader(template)); + } + } throw new IllegalStateException("Template source not available"); } From f23faa26f7ac7926a18bbc1eaf9af5b5052e8f8a Mon Sep 17 00:00:00 2001 From: mariofusco Date: Mon, 11 Nov 2024 17:47:29 +0100 Subject: [PATCH 6/6] Fix deserialization of null maps in reflection-free Jackson deserializers --- .../processor/JacksonDeserializerFactory.java | 16 +++++----- .../jackson/deployment/test/MapWrapper.java | 32 +++++++++++++++++++ .../deployment/test/SimpleJsonResource.java | 7 ++++ .../deployment/test/SimpleJsonTest.java | 16 +++++++++- ...JsonWithReflectionFreeSerializersTest.java | 2 +- 5 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java index fa1fdb002bf0a..ebc47ab056de6 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java @@ -239,18 +239,18 @@ private boolean deserializeObject(ClassInfo classInfo, ResultHandle objHandle, C ResultHandle nextField = loopCreator .invokeInterfaceMethod(ofMethod(Iterator.class, "next", Object.class), fieldsIterator); ResultHandle mapEntry = loopCreator.checkCast(nextField, Map.Entry.class); - ResultHandle fieldName = loopCreator - .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); ResultHandle fieldValue = loopCreator.checkCast(loopCreator .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getValue", Object.class), mapEntry), JsonNode.class); - loopCreator.ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) - .trueBranch().continueScope(loopCreator); + BytecodeCreator fieldReader = loopCreator + .ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) + .falseBranch(); + + ResultHandle fieldName = fieldReader + .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); + Switch.StringSwitch strSwitch = fieldReader.stringSwitch(fieldName); - Set deserializedFields = new HashSet<>(); - ResultHandle deserializationContext = deserialize.getMethodParam(1); - Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); - return deserializeFields(classCreator, classInfo, deserializationContext, objHandle, fieldValue, deserializedFields, + return deserializeFields(classCreator, classInfo, deserialize.getMethodParam(1), objHandle, fieldValue, new HashSet<>(), strSwitch, parseTypeParameters(classInfo, classCreator)); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java new file mode 100644 index 0000000000000..6bc5bda55d642 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.Map; + +public class MapWrapper { + + private String name; + private Map properties; + + public MapWrapper() { + } + + public MapWrapper(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 855c43625c09e..861f01ce08a96 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -123,6 +123,13 @@ public StateRecord echoDog(StateRecord stateRecord) { return stateRecord; } + @POST + @Path("/null-map-echo") + @Consumes(MediaType.APPLICATION_JSON) + public MapWrapper echoNullMap(MapWrapper mapWrapper) { + return mapWrapper; + } + @EnableSecureSerialization @GET @Path("/abstract-cat") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index d2f22569f9a7a..a5fa4d498c923 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -36,7 +36,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -733,4 +733,18 @@ public void testRecordEcho() { assertTrue(first >= 0); assertEquals(first, last); } + + @Test + public void testNullMapEcho() { + RestAssured + .with() + .body(new MapWrapper("test")) + .contentType("application/json; charset=utf-8") + .post("/simple/null-map-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("properties", Matchers.nullValue()); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java index 65dec05aa59a4..10ea3d373ce91 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java @@ -25,7 +25,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n" +