diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml
index d8e5dfa5af4497..3217d4787efdb1 100644
--- a/.github/workflows/ci-actions-incremental.yml
+++ b/.github/workflows/ci-actions-incremental.yml
@@ -1184,7 +1184,7 @@ jobs:
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.native_matrix) }}
- runs-on: ${{matrix.os-name}}
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 07f3499ebe3e48..6ed8fa7d562cca 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -17,6 +17,6 @@
io.quarkus.develocityquarkus-project-develocity-extension
- 1.1.3
+ 1.1.4
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index f2fcc26d1c0cab..c3a990a3307b4b 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -20,10 +20,10 @@
1.0.195.0.03.0.2
- 3.2.0
+ 3.2.11.3.21
- 1.1.6
+ 1.1.72.1.5.Final3.1.3.Final6.2.9.Final
@@ -147,7 +147,7 @@
2.13.141.2.3
- 3.12.0
+ 3.13.02.17.03.0.03.1.0
@@ -160,7 +160,7 @@
3.2.04.2.13.0.6.Final
- 10.16.0
+ 10.17.03.0.34.27.0
@@ -198,7 +198,7 @@
1.11.32.5.10.Final0.1.18.Final
- 1.20.0
+ 1.20.13.4.02.0.2
@@ -6443,16 +6443,11 @@
io.opentelemetry.semconvopentelemetry-semconv${opentelemetry-semconv.version}
-
-
- io.opentelemetry
- opentelemetry-bom
-
-
- io.opentelemetry
- opentelemetry-api
-
-
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv-incubating
+ ${opentelemetry-semconv.version}
diff --git a/build-parent/pom.xml b/build-parent/pom.xml
index d1e227acdf8586..2c8533ab5a5622 100644
--- a/build-parent/pom.xml
+++ b/build-parent/pom.xml
@@ -28,7 +28,7 @@
${scala-maven-plugin.version}
- 3.2.0
+ 3.2.11.0.02.5.13
@@ -107,13 +107,13 @@
7.3.0
- 2.35.0
+ 2.36.02.0.0
- 0.44.0
+ 0.45.03.7.0
@@ -539,36 +539,6 @@
jandex-maven-plugin${jandex.version}
-
- net.revelc.code.formatter
- formatter-maven-plugin
-
-
- quarkus-ide-config
- io.quarkus
- ${project.version}
-
-
-
-
- .cache/formatter-maven-plugin-${formatter-maven-plugin.version}
- eclipse-format.xml
- LF
- ${format.skip}
-
-
-
- net.revelc.code
- impsort-maven-plugin
-
-
- .cache/impsort-maven-plugin-${impsort-maven-plugin.version}
- java.,javax.,jakarta.,org.,com.
- *
- ${format.skip}
- true
-
- maven-resources-plugin
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java
new file mode 100644
index 00000000000000..500a84f90b0f17
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java
@@ -0,0 +1,45 @@
+package io.quarkus.deployment.logging;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.CompositeIndex;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+/**
+ * Contains information to decorate the Log output. Can be used by extensions that output the log / stacktraces,
+ * for example the error page.
+ *
+ * Also see io.quarkus.runtime.logging.DecorateStackUtil to assist with the decoration
+ */
+public final class LoggingDecorateBuildItem extends SimpleBuildItem {
+ private final Path srcMainJava;
+ private final CompositeIndex knowClassesIndex;
+
+ public LoggingDecorateBuildItem(Path srcMainJava, CompositeIndex knowClassesIndex) {
+ this.srcMainJava = srcMainJava;
+ this.knowClassesIndex = knowClassesIndex;
+ }
+
+ public Path getSrcMainJava() {
+ return srcMainJava;
+ }
+
+ public CompositeIndex getKnowClassesIndex() {
+ return knowClassesIndex;
+ }
+
+ public List getKnowClasses() {
+ List knowClasses = new ArrayList<>();
+ Collection knownClasses = knowClassesIndex.getKnownClasses();
+ for (ClassInfo ci : knownClasses) {
+ knowClasses.add(ci.name().toString());
+ }
+ return knowClasses;
+ }
+
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java
index 8781a258c03a6c..af6a515f7be176 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java
@@ -1,6 +1,8 @@
package io.quarkus.deployment.logging;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -45,6 +47,8 @@
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.bootstrap.logging.InitialConfigurator;
+import io.quarkus.bootstrap.model.ApplicationModel;
+import io.quarkus.bootstrap.workspace.WorkspaceModule;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.IsNormal;
@@ -86,6 +90,8 @@
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.deployment.metrics.MetricsFactoryConsumerBuildItem;
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
+import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
+import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.deployment.util.JandexUtil;
@@ -102,11 +108,13 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.logging.LoggingFilter;
+import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.configuration.ConfigInstantiator;
import io.quarkus.runtime.console.ConsoleRuntimeConfig;
import io.quarkus.runtime.logging.CategoryBuildTimeConfig;
import io.quarkus.runtime.logging.CleanupFilterConfig;
+import io.quarkus.runtime.logging.DecorateStackUtil;
import io.quarkus.runtime.logging.DiscoveredLogComponents;
import io.quarkus.runtime.logging.InheritableLevel;
import io.quarkus.runtime.logging.LogBuildTimeConfig;
@@ -370,14 +378,25 @@ private DiscoveredLogComponents discoverLogComponents(IndexView index) {
void setupStackTraceFormatter(ApplicationArchivesBuildItem item, EffectiveIdeBuildItem ideSupport,
BuildSystemTargetBuildItem buildSystemTargetBuildItem,
List exceptionNotificationBuildItems,
- CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem) {
+ CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem,
+ CurateOutcomeBuildItem curateOutcomeBuildItem,
+ OutputTargetBuildItem outputTargetBuildItem,
+ LaunchModeBuildItem launchMode,
+ LogBuildTimeConfig logBuildTimeConfig,
+ BuildProducer loggingDecorateProducer) {
List indexList = new ArrayList<>();
for (ApplicationArchive i : item.getAllApplicationArchives()) {
if (i.getResolvedPaths().isSinglePath() && Files.isDirectory(i.getResolvedPaths().getSinglePath())) {
indexList.add(i.getIndex());
}
}
+ Path srcMainJava = getSourceRoot(curateOutcomeBuildItem.getApplicationModel(),
+ outputTargetBuildItem.getOutputDirectory());
+
CompositeIndex index = CompositeIndex.create(indexList);
+
+ loggingDecorateProducer.produce(new LoggingDecorateBuildItem(srcMainJava, index));
+
//awesome/horrible hack
//we know from the index which classes are part of the current application
//we add ANSI codes for bold and underline to their names to display them more prominently
@@ -393,6 +412,15 @@ public void accept(LogRecord logRecord, Consumer logRecordConsumer) {
var elem = stackTrace[i];
if (index.getClassByName(DotName.createSimple(elem.getClassName())) != null) {
lastUserCode = stackTrace[i];
+
+ if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT)
+ && logBuildTimeConfig.decorateStacktraces) {
+ String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem);
+ if (decoratedString != null) {
+ logRecord.setMessage(logRecord.getMessage() + "\n\n" + decoratedString + "\n\n");
+ }
+ }
+
stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(),
elem.getModuleVersion(),
MessageFormat.UNDERLINE + MessageFormat.BOLD + elem.getClassName()
@@ -665,6 +693,24 @@ ConsoleCommandBuildItem logConsoleCommand() {
return new ConsoleCommandBuildItem(new LogCommand());
}
+ private Path getSourceRoot(ApplicationModel applicationModel, Path target) {
+ WorkspaceModule workspaceModule = applicationModel.getAppArtifact().getWorkspaceModule();
+ if (workspaceModule != null) {
+ return workspaceModule.getModuleDir().toPath().resolve(SRC_MAIN_JAVA);
+ }
+
+ if (target != null) {
+ var baseDir = target.getParent();
+ if (baseDir == null) {
+ baseDir = target;
+ }
+ return baseDir.resolve(SRC_MAIN_JAVA);
+ }
+ return Paths.get(SRC_MAIN_JAVA);
+ }
+
+ private static final String SRC_MAIN_JAVA = "src/main/java";
+
@GroupCommandDefinition(name = "log", description = "Logging Commands")
public static class LogCommand implements GroupCommand {
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java
index 48c18f868b7b21..d16e5ff7e94afe 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java
@@ -429,13 +429,15 @@ public void run() {
} finally {
stateLock.unlock();
}
- if (currentApplication.isStarted()) {
+ //take a reliable reference before changing the application state:
+ final Application app = currentApplication;
+ if (app.isStarted()) {
// On CLI apps, SIGINT won't call io.quarkus.runtime.Application#stop(),
// making the awaitShutdown() below block the application termination process
// It should be a noop if called twice anyway
- currentApplication.stop();
+ app.stop();
}
- currentApplication.awaitShutdown();
+ app.awaitShutdown();
currentApplication = null;
System.out.flush();
System.err.flush();
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java
index 22b3cce91e9f25..e041748577ffa9 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java
@@ -3,11 +3,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import io.quarkus.dev.config.CurrentConfig;
+import io.quarkus.runtime.logging.DecorateStackUtil;
import io.quarkus.runtime.util.ExceptionUtil;
public class TemplateHtmlBuilder {
@@ -146,6 +148,11 @@ public class TemplateHtmlBuilder {
private static final String STACKTRACE_DISPLAY_DIV = "";
+ private static final String BRSTI = "___begin_relative_stack_trace_item___";
+ private static final String ERSTI = "___end_relative_stack_trace_item___";
+
+ private static final String OPEN_IDE_LINK = "
";
+
private static final String ERROR_STACK = "
\n" +
"
The stacktrace below is the original. " +
"See the stacktrace in reversed order (root-cause first)"
@@ -159,6 +166,7 @@ public class TemplateHtmlBuilder {
"
%1$s
\n" +
"
\n";
+ private static final String DECORATE_DIV = "
%s
";
private static final String CONFIG_EDITOR_HEAD = "
The following incorrect config values were detected:
" +
"
");
+ rootFirst = rootFirst.replace(BRSTI,
+ String.format(OPEN_IDE_LINK, className, type, lineNumber));
+ rootFirst = rootFirst.replace(ERSTI, "");
+ }
+
+ result.append(String.format(ERROR_STACK, original));
+ result.append(String.format(ERROR_STACK_REVERSED, rootFirst));
+ result.append(STACKTRACE_DISPLAY_DIV);
+
+ throwable.setStackTrace(originalStackTrace);
+ }
return this;
}
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java
new file mode 100644
index 00000000000000..c193c4e4334f40
--- /dev/null
+++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java
@@ -0,0 +1,93 @@
+package io.quarkus.runtime.logging;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+public class DecorateStackUtil {
+
+ public static String getDecoratedString(final Throwable throwable, String srcMainJava, List knowClasses) {
+ if (srcMainJava != null) {
+ return DecorateStackUtil.getDecoratedString(throwable, Path.of(srcMainJava), knowClasses);
+ }
+ return null;
+ }
+
+ public static String getDecoratedString(final Throwable throwable, Path srcMainJava, List knowClasses) {
+ if (knowClasses != null && !knowClasses.isEmpty() && throwable != null) {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ for (int i = 0; i < stackTrace.length; ++i) {
+ StackTraceElement elem = stackTrace[i];
+ if (knowClasses.contains(elem.getClassName())) {
+ String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem);
+ if (decoratedString != null) {
+ return decoratedString;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static String getDecoratedString(Path srcMainJava, StackTraceElement stackTraceElement) {
+ int lineNumber = stackTraceElement.getLineNumber();
+ if (lineNumber > 0 && srcMainJava != null) {
+ String fullJavaFileName = getFullPath(stackTraceElement.getClassName(), stackTraceElement.getFileName());
+ Path f = srcMainJava.resolve(fullJavaFileName);
+ try {
+ List contextLines = DecorateStackUtil.getRelatedLinesInSource(f, lineNumber, 2);
+ if (contextLines != null) {
+ String header = "Exception in " + stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber();
+ return header + "\n" + String.join("\n", contextLines);
+ }
+ } catch (IOException e) {
+ // Could not find the source for some reason. Just return nothing then
+ }
+ }
+ return null;
+ }
+
+ private static List getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException {
+ if (Files.exists(filePath)) {
+ List resultLines = new ArrayList<>();
+ Deque contextQueue = new ArrayDeque<>(2 * contextRange + 1);
+ try (BufferedReader reader = Files.newBufferedReader(filePath)) {
+ String line;
+ int currentLine = 1;
+ while ((line = reader.readLine()) != null) {
+ if (currentLine >= lineNumber - contextRange) {
+ String ln = String.valueOf(currentLine);
+ if (currentLine == lineNumber) {
+ ln = "-> " + ln + " ";
+ } else {
+ ln = " " + ln + " ";
+ }
+
+ contextQueue.add("\t" + ln + line);
+ }
+ if (currentLine >= lineNumber + contextRange) {
+ break;
+ }
+ currentLine++;
+ }
+ resultLines.addAll(contextQueue);
+ }
+ return resultLines;
+ }
+ return null;
+ }
+
+ private static String getFullPath(String fullClassName, String fileName) {
+ int lastDotIndex = fullClassName.lastIndexOf(".");
+ String packageName = fullClassName.substring(0, lastDotIndex);
+ String path = packageName.replace('.', '/');
+ return path + "/" + fileName;
+ }
+
+}
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java
index 0f9914985fccf9..203a0d7f654b2f 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java
@@ -23,6 +23,12 @@ public class LogBuildTimeConfig {
@ConfigItem(defaultValue = "DEBUG")
public Level minLevel;
+ /**
+ * This will decorate the stacktrace in dev mode to show the line in the code that cause the exception
+ */
+ @ConfigItem(defaultValue = "true")
+ public Boolean decorateStacktraces;
+
/**
* Minimum logging categories.
*
diff --git a/core/runtime/src/main/resources/META-INF/template-html-builder.css b/core/runtime/src/main/resources/META-INF/template-html-builder.css
index 7760fd6de8743c..6cf8325e64a5eb 100644
--- a/core/runtime/src/main/resources/META-INF/template-html-builder.css
+++ b/core/runtime/src/main/resources/META-INF/template-html-builder.css
@@ -114,11 +114,28 @@ h3 {
display: none;
}
+.decorate {
+ font-size: 1.3em;
+ line-height: 1.5;
+}
+
.stacktrace {
font-size: 1.3em;
line-height: 1.5;
+ color: hsla(3, 89%, 42%, 1.0);
+}
+
+.rel-stacktrace-item {
+ font-weight: 400;
+ display: inline;
}
+.rel-stacktrace-item:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+
a, a:visited, a:focus, a:active {
text-decoration: none; color:hsla(211, 63%, 54%, 1.0);
}
diff --git a/devtools/project-core-extension-codestarts/pom.xml b/devtools/project-core-extension-codestarts/pom.xml
index 61564e2b800dbe..7552c88070c838 100644
--- a/devtools/project-core-extension-codestarts/pom.xml
+++ b/devtools/project-core-extension-codestarts/pom.xml
@@ -61,6 +61,7 @@
--typebasic--no-daemon
+ --overwritetarget/classes/gradle-wrappertrue
diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc
index f206e292d6e4fb..e73e092d1ae0c7 100644
--- a/docs/src/main/asciidoc/building-native-image.adoc
+++ b/docs/src/main/asciidoc/building-native-image.adoc
@@ -538,7 +538,7 @@ The project generation has also provided a `Dockerfile.native` in the `src/main/
[source,dockerfile]
----
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
diff --git a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc
index e65e0cf8e3baa0..e5e5fb99a75390 100644
--- a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc
+++ b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc
@@ -76,7 +76,7 @@ Create a file called `src/main/resources/json-schema.json` with the schema for o
"type": "string",
"description": "The movie's title."
},
- "yeay": {
+ "year": {
"type": "integer",
"description": "The movie's year."
}
diff --git a/docs/src/main/asciidoc/opentelemetry-metrics.adoc b/docs/src/main/asciidoc/opentelemetry-metrics.adoc
index d2b93e98d33bed..e7a45bfd829c02 100644
--- a/docs/src/main/asciidoc/opentelemetry-metrics.adoc
+++ b/docs/src/main/asciidoc/opentelemetry-metrics.adoc
@@ -19,7 +19,6 @@ metrics for interactive web applications.
- If you search more information about OpenTelemetry Tracing, please refer to the xref:opentelemetry-tracing.adoc[OpenTelemetry Tracing Guide].
====
-
== Prerequisites
:prerequisites-docker-compose:
@@ -40,7 +39,8 @@ The solution is located in the `opentelemetry-quickstart` link:{quickstarts-tree
== Creating the Maven project
-First, we need a new project. Create a new project with the following command:
+First, we need a new project.
+Create a new project with the following command:
:create-app-artifact-id: opentelemetry-quickstart
:create-app-extensions: rest,quarkus-opentelemetry
@@ -114,13 +114,13 @@ public class MetricResource {
}
----
-Quarkus is not currently producing metrics out of the box. Here we are creating a counter for the number of invocations of the `hello()` method.
+Quarkus is not currently producing metrics out of the box.
+Here we are creating a counter for the number of invocations of the `hello()` method.
<1> Constructor injection of the `Meter` instance.
<2> Create a `LongCounter` named `hello-metrics` with a description and unit.
<3> Increment the counter by one for each invocation of the `hello()` method.
-
=== Create the configuration
There are no mandatory configurations for the extension to work.
@@ -135,9 +135,12 @@ quarkus.otel.exporter.otlp.metrics.endpoint=http://localhost:4317 // <3>
quarkus.otel.exporter.otlp.metrics.headers=authorization=Bearer my_secret // <4>
----
-<1> All metrics created from the application will include an OpenTelemetry `Resource` indicating the metrics was created by the `myservice` application. If not set, it will default to the artifact id.
-<2> Enable the OpenTelemetry metrics. Must be set at build time.
-<3> gRPC endpoint to send the metrics. If not set, it will default to `http://localhost:4317`.
+<1> All metrics created from the application will include an OpenTelemetry `Resource` indicating the metrics was created by the `myservice` application.
+If not set, it will default to the artifact id.
+<2> Enable the OpenTelemetry metrics.
+Must be set at build time.
+<3> gRPC endpoint to send the metrics.
+If not set, it will default to `http://localhost:4317`.
<4> Optional gRPC headers commonly used for authentication.
To configure the connection using the same properties for all signals, please check the base xref:opentelemetry.adoc#create-the-configuration[configuration section of the OpenTelemetry guide].
@@ -153,7 +156,8 @@ First we need to start a system to visualise the OpenTelemetry data.
==== Grafana-OTel-LGTM dev service
You can use the xref:observability-devservices-lgtm.adoc[Grafana-OTel-LGTM] devservice.
-This Dev service includes a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics. Also provides and OTel collector to receive the data.
+This Dev service includes a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics.
+Also provides and OTel collector to receive the data.
==== Logging exporter
@@ -164,12 +168,16 @@ quarkus.otel.metrics.exporter=logging <1>
quarkus.otel.metric.export.interval=10000ms <2>
----
-<1> Set the exporter to `logging`. Normally you don't need to set this. The default is `cdi`.
-<2> Set the interval to export the metrics. The default is `1m`, which is too long for debugging.
+<1> Set the exporter to `logging`.
+Normally you don't need to set this.
+The default is `cdi`.
+<2> Set the interval to export the metrics.
+The default is `1m`, which is too long for debugging.
=== Start the application
-Now we are ready to run our application. If using `application.properties` to configure the tracer:
+Now we are ready to run our application.
+If using `application.properties` to configure the tracer:
include::{includes}/devtools/dev.adoc[]
@@ -187,7 +195,8 @@ $ curl http://localhost:8080/hello-metrics
hello-metrics
----
-When using the logger exporter, metrics will be printed to the console. This is a pretty printed example:
+When using the logger exporter, metrics will be printed to the console.
+This is a pretty printed example:
[source,json]
----
{
@@ -260,8 +269,10 @@ When using the logger exporter, metrics will be printed to the console. This is
<1> Resource attributes common to all telemetry data.
<2> Instrumentation scope is allways `io.quarkus.opentelemetry`
<3> The name, description and unit of the metric you defined in the constructor of the `MetricResource` class.
-<4> The value of the metric. 3 invocations were made until now.
-<5> Exemplars additional tracing information about the metric. In this case, the traceId and spanId of one os the request that triggered the metric, since it was last sent.
+<4> The value of the metric.
+3 invocations were made until now.
+<5> Exemplars additional tracing information about the metric.
+In this case, the traceId and spanId of one os the request that triggered the metric, since it was last sent.
Hit `CTRL+C` or type `q` to stop the application.
@@ -269,11 +280,14 @@ Hit `CTRL+C` or type `q` to stop the application.
=== OpenTelemetry Metrics vs Micrometer Metrics
-Metrics are single numerical measurements, often have additional data captured with them. This ancillary data is used to group or aggregate metrics for analysis.
+Metrics are single numerical measurements, often have additional data captured with them.
+This ancillary data is used to group or aggregate metrics for analysis.
Pretty much like in the xref:telemetry-micrometer.adoc#create-your-own-metrics[Quarkus Micrometer extension], you can create your own metrics using the OpenTelemetry API and the concepts are analogous.
-The OpenTelemetry API provides a `Meter` interface to create metrics instead of a Registry. The `Meter` interface is the entry point for creating metrics. It provides methods to create counters, gauges, and histograms.
+The OpenTelemetry API provides a `Meter` interface to create metrics instead of a Registry.
+The `Meter` interface is the entry point for creating metrics.
+It provides methods to create counters, gauges, and histograms.
Attributes can be added to metrics to add dimensions, pretty much like tags in Micrometer.
@@ -322,20 +336,26 @@ counter.add(1, // <2>
<1> Create a `LongCounter` named `hello-metrics` with a description and unit.
<2> Increment the counter by one.
-<3> Add an attribute to the counter. This will create a dimension called `attribute.name` with value `attribute value`.
+<3> Add an attribute to the counter.
+This will create a dimension called `attribute.name` with value `attribute value`.
-IMPORTANT: Each unique combination of metric name and dimension produces a unique time series. Using an unbounded set of dimensional data (many different values like a userId) can lead to a "cardinality explosion", an exponential increase in the creation of new time series. Avoid!
+IMPORTANT: Each unique combination of metric name and dimension produces a unique time series.
+Using an unbounded set of dimensional data (many different values like a userId) can lead to a "cardinality explosion", an exponential increase in the creation of new time series.
+Avoid!
OpenTelemetry provides many other types of Counters: `LongUpDownCounter`, `DoubleCounter`, `DoubleUpDownCounter` and also Observable, async counters like `ObservableLongCounter`, `ObservableDoubleCounter`, `ObservableLongUpDownCounter` and `ObservableDoubleUpDownCounter`.
For more details please refer to the https://opentelemetry.io/docs/languages/java/instrumentation/#using-counters[OpenTelemetry Java documentation about Counters].
=== Gauges
-Observable Gauges should be used to measure non-additive values. A value that can increase or decrease over time, like the speedometer on a car. Gauges can be useful when monitoring the statistics for a cache or collection.
+Observable Gauges should be used to measure non-additive values.
+A value that can increase or decrease over time, like the speedometer on a car.
+Gauges can be useful when monitoring the statistics for a cache or collection.
-With this metric you provide a function to be periodically probed by a callback. The value returned by the function is the value of the gauge.
+With this metric you provide a function to be periodically probed by a callback.
+The value returned by the function is the value of the gauge.
-The default gauge records `Double`values, but if you want to record `Long` values, you can use
+The default gauge records `Double` values, but if you want to record `Long` values, you can use
[source, java]
----
@@ -351,7 +371,8 @@ meter.gaugeBuilder("jvm.memory.total") // <1>
----
<1> Create a `Gauge` named `jvm.memory.total` with a description and unit.
<2> If you want to record `Long` values you need this builder method because the default gauge records `Double` values.
-<3> Build the gauge with a callback. An imperative builder is also available.
+<3> Build the gauge with a callback.
+An imperative builder is also available.
<4> Register the function to call to get the value of the gauge.
<5> No added attributes, this time.
@@ -360,7 +381,6 @@ Histograms are synchronous instruments used to measure a distribution of values
It is intended for statistics such as histograms, summaries, and percentile.
The request duration and response payload size are good uses for a histogram.
-
On this section we have a new class, the `HistogramResource` that will create a `LongHistogram`.
[source, java]
@@ -412,9 +432,11 @@ public class HistogramResource {
----
<1> Create a `LongHistogram` named `hello.roll.dice` with a description and unit.
<2> If you want to record `Long` values you need this builder method because the default histogram records `Double` values.
-<3> Set the explicit bucket boundaries for the histogram. The boundaries are inclusive.
+<3> Set the explicit bucket boundaries for the histogram.
+The boundaries are inclusive.
<4> Record the value of the roll.
-<5> Add an attribute to the histogram. This will create a dimension called `attribute.name` with value `value`.
+<5> Add an attribute to the histogram.
+This will create a dimension called `attribute.name` with value `value`.
IMPORTANT: Beware of cardinality explosion.
@@ -425,7 +447,8 @@ $ curl http://localhost:8080/roll-dice
2
----
-If we execute 4 consecutive requests, with results *2,2,3 and 4* this will produce the following output. The `Resource` and `InstrumentationScopeInfo` data are ignored for brevity.
+If we execute 4 consecutive requests, with results *2,2,3 and 4* this will produce the following output.
+The `Resource` and `InstrumentationScopeInfo` data are ignored for brevity.
[source,json]
----
//...
@@ -477,13 +500,16 @@ data=ImmutableHistogramData{
<7> The maximum value recorded.
<8> The explicit bucket boundaries for the histogram.
<9> The number of values recorded in each bucket.
-<10> The list of exemplars with tracing data for the values recorded. We only show 1 of 3 exemplars for brevity.
+<10> The list of exemplars with tracing data for the values recorded.
+We only show 1 of 3 exemplars for brevity.
<11> One of the 2 calls made with the value 2.
=== Differences with the Micrometer API
-- Timers and Distribution Summaries are not available in the OpenTelemetry API. Instead, use Histograms.
-- The OpenTelemetry API does not define annotations for metrics like Micrometer's `@Counted`, `@Timed` or `@MeterTag` You need to manually create the metrics.
+- Timers and Distribution Summaries are not available in the OpenTelemetry API.
+ Instead, use Histograms.
+- The OpenTelemetry API does not define annotations for metrics like Micrometer's `@Counted`, `@Timed` or `@MeterTag`.
+ You need to manually create the metrics.
- OpenTelemetry uses their own https://opentelemetry.io/docs/specs/semconv/[Semantic Conventions] to name metrics and attributes.
=== Resource
@@ -491,7 +517,8 @@ See the main xref:opentelemetry.adoc#resource[OpenTelemetry Guide resources] sec
== Additional instrumentation
-Automatic metrics are not yet provided by the Quarkus OpenTelemetry extension. We plan to bridge the existing Quarkus Micrometer extension metrics to OpenTelemetry in the future.
+Automatic metrics are not yet provided by the Quarkus OpenTelemetry extension.
+We plan to bridge the existing Quarkus Micrometer extension metrics to OpenTelemetry in the future.
== Exporters
See the main xref:opentelemetry.adoc#exporters[OpenTelemetry Guide exporters] section.
diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc
index ba9f0ff6c8b9ab..c9640697f42acc 100644
--- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc
+++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc
@@ -39,7 +39,7 @@ In this case, you need to use a multi-stage `dockerfile` to copy the required li
[source, dockerfile]
----
# First stage - install the dependencies in an intermediate container
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 as BUILD
RUN microdnf install freetype
# Second stage - copy the dependencies
@@ -62,7 +62,7 @@ If you need to have access to the full AWT support, you need more than just `lib
[source, dockerfile]
----
# First stage - install the dependencies in an intermediate container
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 as BUILD
RUN microdnf install freetype fontconfig
# Second stage - copy the dependencies
@@ -112,7 +112,7 @@ To use this base image, use the following `Dockerfile`:
[source, dockerfile]
----
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc
index cafc6486118531..09d9af02496f52 100644
--- a/docs/src/main/asciidoc/rest-client.adoc
+++ b/docs/src/main/asciidoc/rest-client.adoc
@@ -1657,6 +1657,34 @@ quarkus.rest-client.my-client.url=...
NOTE: MicroProfile REST Client specification does not allow setting proxy credentials. In order to specify proxy user and proxy password programmatically, you need to cast your `RestClientBuilder` to `RestClientBuilderImpl`.
+=== Local proxy for dev mode
+
+When using the REST Client in dev mode, Quarkus has the ability to stand up a pass-through proxy which can be used as a target for Wireshark (or similar tools)
+in order to capture all the traffic originating from the REST Client (this really makes sense when the REST Client is used against HTTPS services)
+
+To enable this feature, all that needs to be done is set the `enable-local-proxy` configuration option for the configKey corresponding to the client for which proxying is desired.
+For example:
+
+[source,properties]
+----
+quarkus.rest-client.my-client.enable-local-proxy=true
+----
+
+When a REST Client does not use a config key (for example when it is created programmatically via `QuarkusRestClientBuilder`) then the class name can be used instead.
+For example:
+
+[source,properties]
+----
+quarkus.rest-client."org.acme.SomeClient".enable-local-proxy=true
+----
+
+The port the proxy is listening can be found in startup logs. An example entry is:
+
+[source]
+----
+Started HTTP proxy server on http://localhost:38227 for REST Client 'org.acme.SomeClient'
+----
+
== Package and run the application
Run the application with:
diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc
index f40b865e34917d..7a45e13eac1a92 100644
--- a/docs/src/main/asciidoc/rest.adoc
+++ b/docs/src/main/asciidoc/rest.adoc
@@ -2271,6 +2271,40 @@ By default, methods annotated with `@ServerExceptionMapper` do **not** run CDI i
Users however can opt into interceptors by adding the corresponding annotations to the method.
====
+[TIP]
+====
+When mapping an exception to a `@ServerExceptionMapper` method, the cause of the exception normally does not come into play.
+
+However, some exception types in Java only serve as wrappers for other exceptions. Often, checked exceptions are wrapped into `RuntimeException` just to not have them declared in method `throws` parameters.
+Working with `CompletionStage` for example, will require `CompletionException`. There are many such exception types that are just wrappers around the real cause of the exception.
+
+If you wish to make sure your exception mapper is called for your exception type even when it is wrapped by one of those wrapper exceptions, you can use `@UnwrapException` on the exception wrapper type:
+
+[source,java]
+----
+public class MyExceptionWrapper extends RuntimeException {
+ public MyExceptionWrapper(Exception cause) {
+ super(cause);
+ }
+}
+----
+
+If you don't control that exception wrapper type, you can place the annotation on any class and specify the exception wrapper types it applies to as annotation parameter:
+
+[source,java]
+----
+@UnwrapException({CompletionException.class, RuntimeException.class})
+public class Mapper {
+
+ @ServerExceptionMapper
+ public Response handleMyException(MyException x) {
+ // ...
+ }
+
+}
+----
+====
+
[NOTE]
====
Εxception mappers defined in REST endpoint classes will only be called if the exception is thrown in the same class. If you want to define global exception mappers,
diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc
index ebfa3a4f345401..943ff8319b1fe4 100644
--- a/docs/src/main/asciidoc/scheduler-reference.adoc
+++ b/docs/src/main/asciidoc/scheduler-reference.adoc
@@ -100,7 +100,7 @@ For example `"0 15 10 * * ?"` fires at 10:15am every day.
[source,java]
----
@Scheduled(cron = "0 15 10 * * ?")
-void fireAt10AmEveryDay() { }
+void fireAt1015AmEveryDay() { }
----
The syntax used in CRON expressions is controlled by the `quarkus.scheduler.cron-type` property.
@@ -359,6 +359,30 @@ class Jobs {
----
<1> The return type `Uni` instructs the scheduler to execute the method on the Vert.x event loop.
+=== How to use multiple scheduler implementations
+
+In some cases, it might be useful to choose a scheduler implementation used to execute a scheduled method.
+However, only one `Scheduler` implementation is used for all scheduled methods by default.
+For example, the `quarkus-quartz` extension provides an implementation that supports clustering but it also removes the simple in-memory implementation from the game.
+Now, if clustering is enabled then it's not possible to define a scheduled method that would be executed locally on a single node.
+Nevertheless, if you set the `quarkus.scheduler.use-composite-scheduler` config property to `true` then a composite `Scheduler` is used instead.
+This means that multiple scheduler implementations are kept running side by side.
+Furthermore, it's possible to chose a specific implementation used to execute a scheduled method using `@Scheduled#executeWith()`.
+
+[source,java]
+----
+class Jobs {
+
+ @Scheduled(cron = "0 15 10 * * ?") <1>
+ void fireAt10AmEveryDay() { }
+
+ @Scheduled(every = "1s", executeWith = Scheduled.SIMPLE) <2>
+ void everySecond() { }
+}
+----
+<1> If the `quarkus-quartz` extension is present then this method will be executed with the Quartz-specific scheduler.
+<2> If `quarkus.scheduler.use-composite-scheduler=true` is set then this method will be executed with the simple in-memory implementation provided by the `quarkus-scheduler` extension.
+
== Scheduler
Quarkus provides a built-in bean of type `io.quarkus.scheduler.Scheduler` that can be injected and used to pause/resume the scheduler and individual scheduled methods identified by a specific `Scheduled#identity()`.
diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc
index cb41a8bc350d04..52c20b03c83e4b 100644
--- a/docs/src/main/asciidoc/spring-data-jpa.adoc
+++ b/docs/src/main/asciidoc/spring-data-jpa.adoc
@@ -599,6 +599,7 @@ An extensive list of examples can be seen in the https://github.com/quarkusio/qu
* Methods of the `org.springframework.data.repository.query.QueryByExampleExecutor` interface - if any of these are invoked, a Runtime exception will be thrown.
* QueryDSL support. No attempt will be made to generate implementations of the QueryDSL related repositories.
+* Using `org.springframework.data.jpa.repository.JpaSpecificationExecutor`
* Customizing the base repository for all repository interfaces in the code base.
** In Spring Data JPA this is done by registering a class that extends `org.springframework.data.jpa.repository.support.SimpleJpaRepository` however in Quarkus this class
is not used at all (since all the necessary plumbing is done at build time). Similar support might be added to Quarkus in the future.
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
index 428b12ad125663..b23ee16b8cb6a9 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java
@@ -40,10 +40,10 @@ void testImage(int javaVersion, int ubiVersion, String imageVersion) {
static Stream imageCombinations() {
return Stream.of(
- Arguments.of(17, 8, "1.19"),
- Arguments.of(21, 8, "1.19"),
- Arguments.of(17, 9, "1.18"),
- Arguments.of(21, 9, "1.18"));
+ Arguments.of(17, 8, "1.20"),
+ Arguments.of(21, 8, "1.20"),
+ Arguments.of(17, 9, "1.20"),
+ Arguments.of(21, 9, "1.20"));
}
@Test
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java
index c0ede25661708b..a19842053b3d46 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java
@@ -39,8 +39,8 @@ void testImage(int ubiVersion, int javaVersion, String imageVersion) {
static Stream imageCombinations() {
return Stream.of(
- Arguments.of(8, 17, "8.9"),
- Arguments.of(8, 21, "8.9"),
+ Arguments.of(8, 17, "8.10"),
+ Arguments.of(8, 21, "8.10"),
Arguments.of(9, 17, "9.4"),
Arguments.of(9, 21, "9.4"));
}
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17
index 77d59a96bc997e..777116ae52f3e4 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17
@@ -1,4 +1,4 @@
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
ARG JAVA_PACKAGE=java-17-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21
index ee1e916b5d106a..acc768a6f9ca17 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21
@@ -1,4 +1,4 @@
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
ARG JAVA_PACKAGE=java-21-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime
index a06add4a4733ea..e6463565e14821 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime
@@ -1,4 +1,4 @@
-FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19
+FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.20
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime
index 0a470b183b8da5..c2dafccd009908 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime
@@ -1,5 +1,5 @@
# Use Java 21 base image
-FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19
+FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.20
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime
index bc09884236f94c..f83ad03e05062a 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime
@@ -1,4 +1,4 @@
-FROM registry.access.redhat.com/ubi9/openjdk-17-runtime:1.18
+FROM registry.access.redhat.com/ubi9/openjdk-17-runtime:1.20
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime
index d50d4344121960..ffa2b1f33d7343 100644
--- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime
+++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime
@@ -1,5 +1,5 @@
# Use Java 21 base image
-FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.18
+FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.20
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java
index a6792a12483669..3e442d337425ed 100644
--- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java
+++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java
@@ -17,9 +17,9 @@ public class ContainerImageJibConfig {
/**
* The base image to be used when a container image is being produced for the jar build.
*
- * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19}
+ * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.20}
* is used as the default.
- * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19} is used as the default.
+ * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.20} is used as the default.
*/
@ConfigItem
public Optional baseJvmImage;
diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
index 38d9bf90817ad0..3b93e633ec9463 100644
--- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
+++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java
@@ -98,9 +98,9 @@ public class JibProcessor {
private static final String OPENJDK_PREFIX = "openjdk";
private static final String RUNTIME_SUFFIX = "runtime";
- private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX,
+ private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.20", UBI8_PREFIX, OPENJDK_PREFIX,
RUNTIME_SUFFIX);
- private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX,
+ private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.20", UBI8_PREFIX, OPENJDK_PREFIX,
RUNTIME_SUFFIX);
// The source for this can be found at https://github.com/jboss-container-images/openjdk/blob/ubi8/modules/run/artifacts/opt/jboss/container/java/run/run-java.sh
diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java
index df20d64f6c5964..d37b1679db2010 100644
--- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java
+++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java
@@ -15,8 +15,8 @@
@ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME)
public class ContainerImageOpenshiftConfig {
- public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19";
- public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19";
+ public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.20";
+ public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.20";
public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0";
public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application";
@@ -47,9 +47,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion
* The value of this property is used to create an ImageStream for the builder image used in the Openshift build.
* When it references images already available in the internal Openshift registry, the corresponding streams are used
* instead.
- * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19}
+ * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.20}
* is used as the default.
- * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default.
+ * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.20} is used as the default.
*/
@ConfigItem
public Optional baseJvmImage;
diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java
index 675519cd28f9a6..7fe3ac81cffba1 100644
--- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java
+++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java
@@ -12,8 +12,8 @@
@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
public class S2iConfig {
- public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19";
- public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19";
+ public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.20";
+ public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.20";
public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0";
public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application";
@@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion
/**
* The base image to be used when a container image is being produced for the jar build.
*
- * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19}
+ * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.20}
* is used as the default.
- * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default.
+ * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.20} is used as the default.
*/
@ConfigItem
public Optional baseJvmImage;
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 83b262b1470310..127d583a8bd3c1 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
@@ -64,10 +64,9 @@
import io.quarkus.quartz.runtime.jdbc.QuarkusPostgreSQLDelegate;
import io.quarkus.quartz.runtime.jdbc.QuarkusStdJDBCDelegate;
import io.quarkus.runtime.configuration.ConfigurationException;
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.scheduler.deployment.SchedulerImplementationBuildItem;
-/**
- *
- */
public class QuartzProcessor {
private static final DotName JOB = DotName.createSimple(Job.class.getName());
@@ -77,6 +76,11 @@ FeatureBuildItem feature() {
return new FeatureBuildItem(Feature.QUARTZ);
}
+ @BuildStep
+ SchedulerImplementationBuildItem implementation() {
+ return new SchedulerImplementationBuildItem(Scheduled.QUARTZ, DotName.createSimple(QuartzSchedulerImpl.class), 1);
+ }
+
@BuildStep
AdditionalBeanBuildItem beans() {
return new AdditionalBeanBuildItem(QuartzSchedulerImpl.class);
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java
index 9a2943c1ab78ac..410018b410a2b6 100644
--- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java
@@ -86,10 +86,10 @@ public void testDependentBeanJobDestroyed() throws SchedulerException, Interrupt
@Test
public void testDependentBeanJobWithRefire() throws SchedulerException, InterruptedException {
- // 5 one-off jobs should trigger construction/execution/destruction 10 times in total
- CountDownLatch execLatch = service.initExecuteLatch(10);
- CountDownLatch constructLatch = service.initConstructLatch(10);
- CountDownLatch destroyedLatch = service.initDestroyedLatch(10);
+ // 5 one-off jobs should trigger construction/execution/destruction 5 times in total
+ CountDownLatch execLatch = service.initExecuteLatch(5);
+ CountDownLatch constructLatch = service.initConstructLatch(5);
+ CountDownLatch destroyedLatch = service.initDestroyedLatch(5);
for (int i = 0; i < 5; i++) {
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger" + i, "myRefiringGroup")
@@ -104,10 +104,10 @@ public void testDependentBeanJobWithRefire() throws SchedulerException, Interrup
assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount());
assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount());
- // repeating job triggering three times; we expect six beans to exist for that due to refires
- execLatch = service.initExecuteLatch(6);
- constructLatch = service.initConstructLatch(6);
- destroyedLatch = service.initDestroyedLatch(6);
+ // repeating job triggering three times; re-fires should NOT recreate the bean instance
+ execLatch = service.initExecuteLatch(3);
+ constructLatch = service.initConstructLatch(3);
+ destroyedLatch = service.initDestroyedLatch(3);
JobDetail job = JobBuilder.newJob(RefiringJob.class)
.withIdentity("myRepeatingJob", "myRefiringGroup")
.build();
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java
new file mode 100644
index 00000000000000..3f0a82a31277f3
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java
@@ -0,0 +1,108 @@
+package io.quarkus.quartz.test.composite;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+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.scheduler.Scheduler;
+import io.quarkus.scheduler.runtime.Constituent;
+import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.common.annotation.Identifier;
+
+public class CompositeJobDefinitionTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ })
+ .overrideConfigKey("quarkus.scheduler.use-composite-scheduler", "true")
+ .overrideConfigKey("quarkus.scheduler.start-mode", "forced");
+
+ @Constituent
+ QuartzScheduler quartz;
+
+ @Constituent
+ @Identifier("SIMPLE")
+ Scheduler simple;
+
+ @Inject
+ Scheduler composite;
+
+ static CountDownLatch simpleLatch = new CountDownLatch(1);
+ static CountDownLatch quartzLatch = new CountDownLatch(1);
+ static CountDownLatch autoLatch = new CountDownLatch(1);
+
+ static void reset() {
+ simpleLatch = new CountDownLatch(1);
+ quartzLatch = new CountDownLatch(1);
+ autoLatch = new CountDownLatch(1);
+ }
+
+ @Test
+ public void testExecution() throws InterruptedException {
+
+ assertEquals("Scheduler implementation not available: bar",
+ assertThrows(IllegalArgumentException.class, () -> composite.newJob("foo").setExecuteWith("bar")).getMessage());
+
+ composite.newJob("simple")
+ .setInterval("1s")
+ .setExecuteWith(Scheduled.SIMPLE)
+ .setTask(se -> {
+ simpleLatch.countDown();
+ }).schedule();
+
+ composite.newJob("quartz")
+ .setInterval("1s")
+ .setExecuteWith(Scheduled.QUARTZ)
+ .setTask(se -> {
+ quartzLatch.countDown();
+ }).schedule();
+
+ composite.newJob("auto")
+ .setInterval("1s")
+ .setTask(se -> {
+ autoLatch.countDown();
+ }).schedule();
+
+ assertTrue(simpleLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(quartzLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(autoLatch.await(5, TimeUnit.SECONDS));
+
+ assertNull(quartz.getScheduledJob("simple"));
+ assertNotNull(quartz.getScheduledJob("quartz"));
+ assertNotNull(quartz.getScheduledJob("auto"));
+
+ assertNotNull(simple.getScheduledJob("simple"));
+ assertNull(simple.getScheduledJob("quartz"));
+ assertNull(simple.getScheduledJob("auto"));
+
+ assertNotNull(composite.getScheduledJob("quartz"));
+ assertNotNull(composite.getScheduledJob("auto"));
+ assertNotNull(composite.getScheduledJob("simple"));
+
+ composite.pause();
+ reset();
+ assertFalse(composite.isRunning());
+ assertFalse(simpleLatch.await(2, TimeUnit.SECONDS));
+
+ composite.resume();
+ assertTrue(composite.isRunning());
+ assertTrue(simpleLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(quartzLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(autoLatch.await(5, TimeUnit.SECONDS));
+ }
+
+}
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java
new file mode 100644
index 00000000000000..08a8dd58a85b9f
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java
@@ -0,0 +1,36 @@
+package io.quarkus.quartz.test.composite;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.scheduler.Scheduled;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class CompositeSchedulerNotUsedTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(Jobs.class))
+ .assertException(t -> {
+ assertThat(t).cause().isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(
+ "The required scheduler implementation is not available because the composite scheduler is not used: SIMPLE");
+ });
+
+ @Test
+ public void test() {
+ fail();
+ }
+
+ static class Jobs {
+
+ @Scheduled(every = "1s", executeWith = Scheduled.SIMPLE)
+ void quartz() {
+ }
+
+ }
+}
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java
new file mode 100644
index 00000000000000..d08607b05e317e
--- /dev/null
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java
@@ -0,0 +1,99 @@
+package io.quarkus.quartz.test.composite;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+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.scheduler.Scheduler;
+import io.quarkus.scheduler.runtime.Constituent;
+import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.common.annotation.Identifier;
+
+public class CompositeSchedulerTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root
+ .addClasses(Jobs.class))
+ .overrideConfigKey("quarkus.scheduler.use-composite-scheduler", "true");
+
+ @Constituent
+ QuartzScheduler quartz;
+
+ @Constituent
+ @Identifier("SIMPLE")
+ Scheduler simple;
+
+ @Inject
+ Scheduler composite;
+
+ @Test
+ public void testExecution() throws InterruptedException {
+ assertTrue(Jobs.simpleLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.quartzLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.autoLatch.await(5, TimeUnit.SECONDS));
+
+ assertNull(quartz.getScheduledJob("simple"));
+ assertNotNull(quartz.getScheduledJob("quartz"));
+ assertNotNull(quartz.getScheduledJob("auto"));
+
+ assertNotNull(simple.getScheduledJob("simple"));
+ assertNull(simple.getScheduledJob("quartz"));
+ assertNull(simple.getScheduledJob("auto"));
+
+ assertNotNull(composite.getScheduledJob("quartz"));
+ assertNotNull(composite.getScheduledJob("auto"));
+ assertNotNull(composite.getScheduledJob("simple"));
+
+ composite.pause();
+ Jobs.reset();
+ assertFalse(composite.isRunning());
+ assertFalse(Jobs.simpleLatch.await(2, TimeUnit.SECONDS));
+
+ composite.resume();
+ assertTrue(composite.isRunning());
+ assertTrue(Jobs.simpleLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.quartzLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(Jobs.autoLatch.await(5, TimeUnit.SECONDS));
+ }
+
+ static class Jobs {
+
+ static CountDownLatch simpleLatch = new CountDownLatch(1);
+ static CountDownLatch quartzLatch = new CountDownLatch(1);
+ static CountDownLatch autoLatch = new CountDownLatch(1);
+
+ static void reset() {
+ simpleLatch = new CountDownLatch(1);
+ quartzLatch = new CountDownLatch(1);
+ autoLatch = new CountDownLatch(1);
+ }
+
+ @Scheduled(identity = "simple", every = "1s", executeWith = Scheduled.SIMPLE)
+ void simple() {
+ simpleLatch.countDown();
+ }
+
+ @Scheduled(identity = "quartz", every = "1s", executeWith = Scheduled.QUARTZ)
+ void quartz() {
+ quartzLatch.countDown();
+ }
+
+ @Scheduled(identity = "auto", every = "1s")
+ void auto() {
+ autoLatch.countDown();
+ }
+
+ }
+}
diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java
index f7226ab5d5d4b9..8381694a248cca 100644
--- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java
+++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java
@@ -6,6 +6,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -40,6 +41,7 @@ public class InterruptableJobTest {
static final CountDownLatch INTERRUPT_LATCH = new CountDownLatch(1);
static final CountDownLatch EXECUTE_LATCH = new CountDownLatch(1);
+ static Integer initCounter = 0;
static final CountDownLatch NON_INTERRUPTABLE_EXECUTE_LATCH = new CountDownLatch(1);
static final CountDownLatch NON_INTERRUPTABLE_HOLD_LATCH = new CountDownLatch(1);
@@ -66,7 +68,9 @@ public void testInterruptableJob() throws InterruptedException {
throw new RuntimeException(e);
}
- assertTrue(INTERRUPT_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(INTERRUPT_LATCH.await(3, TimeUnit.SECONDS));
+ // asserts that a single dep. scoped bean instance was used for both, execute() and interrupt() methods
+ assertTrue(initCounter == 1);
}
@Test
@@ -102,6 +106,11 @@ public void testNonInterruptableJob() throws InterruptedException {
@ApplicationScoped
static class MyJob implements InterruptableJob {
+ @PostConstruct
+ public void postConstruct() {
+ initCounter++;
+ }
+
@Override
public void execute(JobExecutionContext context) {
EXECUTE_LATCH.countDown();
diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java
index 4e02136078ef94..8c3e42c1becd07 100644
--- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java
+++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java
@@ -19,19 +19,24 @@
*/
class CdiAwareJob implements InterruptableJob {
- private final Instance extends Job> jobInstance;
+ private final Instance.Handle extends Job> handle;
+ private final Job beanInstance;
- public CdiAwareJob(Instance extends Job> jobInstance) {
- this.jobInstance = jobInstance;
+ public CdiAwareJob(Instance.Handle extends Job> handle) {
+ this.handle = handle;
+ this.beanInstance = handle.get();
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
- Instance.Handle extends Job> handle = jobInstance.getHandle();
+ boolean refire = false;
try {
- handle.get().execute(context);
+ beanInstance.execute(context);
+ } catch (JobExecutionException e) {
+ refire = e.refireImmediately();
+ throw e;
} finally {
- if (handle.getBean().getScope().equals(Dependent.class)) {
+ if (refire != true && handle.getBean().getScope().equals(Dependent.class)) {
handle.destroy();
}
}
@@ -39,16 +44,9 @@ public void execute(JobExecutionContext context) throws JobExecutionException {
@Override
public void interrupt() throws UnableToInterruptJobException {
- Instance.Handle extends Job> handle = jobInstance.getHandle();
// delegate if possible; throw an exception in other cases
if (InterruptableJob.class.isAssignableFrom(handle.getBean().getBeanClass())) {
- try {
- ((InterruptableJob) handle.get()).interrupt();
- } finally {
- if (handle.getBean().getScope().equals(Dependent.class)) {
- handle.destroy();
- }
- }
+ ((InterruptableJob) beanInstance).interrupt();
} else {
throw new UnableToInterruptJobException("Job " + handle.getBean().getBeanClass()
+ " can not be interrupted, since it does not implement " + InterruptableJob.class.getName());
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 7e08b2a596de56..57a79077eb6882 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
@@ -198,7 +198,8 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport
if (!enabled) {
LOGGER.info("Quartz scheduler is disabled by config property and will not be started");
this.scheduler = null;
- } else if (!forceStart && context.getScheduledMethods().isEmpty() && !context.forceSchedulerStart()) {
+ } else if (!forceStart && context.getScheduledMethods(Scheduled.QUARTZ).isEmpty()
+ && !context.forceSchedulerStart()) {
LOGGER.info("No scheduled business methods found - Quartz scheduler will not be started");
this.scheduler = null;
} else {
@@ -232,10 +233,13 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) {
}
};
- for (ScheduledMethod method : context.getScheduledMethods()) {
+ for (ScheduledMethod method : context.getScheduledMethods(Scheduled.QUARTZ)) {
int nameSequence = 0;
for (Scheduled scheduled : method.getSchedules()) {
+ if (!context.matchesImplementation(scheduled, Scheduled.QUARTZ)) {
+ continue;
+ }
String identity = SchedulerUtils.lookUpPropertyValue(scheduled.identity());
if (identity.isEmpty()) {
identity = ++nameSequence + "_" + method.getInvokerClassName();
@@ -345,6 +349,11 @@ public org.quartz.Scheduler getScheduler() {
return scheduler;
}
+ @Override
+ public String implementation() {
+ return Scheduled.QUARTZ;
+ }
+
@Override
public void pause() {
if (!enabled) {
@@ -893,7 +902,7 @@ public Trigger schedule() {
}
scheduled = true;
SyntheticScheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed,
- overdueGracePeriod, concurrentExecution, skipPredicate, timeZone);
+ overdueGracePeriod, concurrentExecution, skipPredicate, timeZone, implementation);
return createJobDefinitionQuartzTrigger(this, scheduled, null);
}
@@ -1246,7 +1255,7 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr
Instance extends Job> instance = jobs.select(jobClass);
if (instance.isResolvable()) {
// This is a job backed by a CDI bean
- return jobWithSpanWrapper(new CdiAwareJob(instance));
+ return jobWithSpanWrapper(new CdiAwareJob(instance.getHandle()));
}
// Instantiate a plain job class
return jobWithSpanWrapper(super.newJob(bundle, Scheduler));
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
index f40f1eeb724aa9..193fd8f588f78a 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
@@ -61,15 +61,17 @@
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
+import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
-import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.CompletedApplicationClassPredicateBuildItem;
import io.quarkus.arc.deployment.QualifierRegistrarBuildItem;
+import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.QualifierRegistrar;
@@ -951,7 +953,7 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis,
BuildProducer incorrectExpressions,
BuildProducer implicitClasses,
BuildProducer expressionMatches,
- BeanDiscoveryFinishedBuildItem beanDiscovery,
+ SynthesisFinishedBuildItem synthesisFinished,
List checkedTemplates,
List templateData,
QuteConfig config,
@@ -970,10 +972,7 @@ public String apply(String id) {
return findTemplatePath(templatesAnalysis, id);
}
};
- // IMPLEMENTATION NOTE:
- // We do not support injection of synthetic beans with names
- // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain
- Map namedBeans = beanDiscovery.beanStream().withName()
+ Map namedBeans = synthesisFinished.beanStream().withName()
.collect(toMap(BeanInfo::getName, Function.identity()));
// Map implicit class -> set of used members
Map> implicitClassToMembersUsed = new HashMap<>();
@@ -2447,9 +2446,7 @@ public boolean test(TypeCheck check) {
@BuildStep
@Record(value = STATIC_INIT)
void initialize(BuildProducer syntheticBeans, QuteRecorder recorder,
- List generatedValueResolvers, List templatePaths,
- Optional templateVariants,
- List templateInitializers,
+ List templatePaths, Optional templateVariants,
TemplateRootsBuildItem templateRoots) {
List templates = new ArrayList<>();
@@ -2475,14 +2472,25 @@ void initialize(BuildProducer syntheticBeans, QuteRecord
}
syntheticBeans.produce(SyntheticBeanBuildItem.configure(QuteContext.class)
- .supplier(recorder.createContext(generatedValueResolvers.stream()
- .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates,
- tags, variants, templateInitializers.stream()
- .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()),
+ .scope(BuiltinScope.SINGLETON.getInfo())
+ .supplier(recorder.createContext(templates,
+ tags, variants,
templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents))
.done());
}
+ @BuildStep
+ @Record(value = STATIC_INIT)
+ void initializeGeneratedClasses(BeanContainerBuildItem beanContainer, QuteRecorder recorder,
+ List generatedValueResolvers,
+ List templateInitializers) {
+ // The generated classes must be initialized after the template expressions are validated in order to break the cycle in the build chain
+ recorder.initializeGeneratedClasses(generatedValueResolvers.stream()
+ .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()),
+ templateInitializers.stream()
+ .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()));
+ }
+
@BuildStep
QualifierRegistrarBuildItem turnLocationIntoQualifier() {
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java
index e3bca7a111002e..6ba4437a63ec80 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java
@@ -14,10 +14,14 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.builder.BuildContext;
+import io.quarkus.builder.BuildStep;
import io.quarkus.qute.Qute;
import io.quarkus.qute.Template;
import io.quarkus.qute.deployment.Hello;
import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.common.annotation.Identifier;
public class InjectNamespaceResolverTest {
@@ -28,7 +32,21 @@ public class InjectNamespaceResolverTest {
.addAsResource(
new StringAsset(
"{inject:hello.ping} != {inject:simple.ping} and {cdi:hello.ping} != {cdi:simple.ping}"),
- "templates/foo.html"));
+ "templates/foo.html"))
+ .addBuildChainCustomizer(bcb -> {
+ bcb.addBuildStep(new BuildStep() {
+ @Override
+ public void execute(BuildContext context) {
+ context.produce(SyntheticBeanBuildItem.configure(String.class)
+ .addQualifier().annotation(Identifier.class).addValue("value", "synthetic").done()
+ .name("synthetic")
+ .creator(mc -> {
+ mc.returnValue(mc.load("Yes!"));
+ })
+ .done());
+ }
+ }).produces(SyntheticBeanBuildItem.class).build();
+ });
@Inject
Template foo;
@@ -45,6 +63,9 @@ public void testInjection() {
assertEquals("pong::<br>",
Qute.fmt("{cdi:hello.ping}::{newLine}").contentType("text/html").data("newLine", " ").render());
assertEquals(2, SimpleBean.DESTROYS.longValue());
+
+ // Test a synthetic named bean injected in a template
+ assertEquals("YES!", Qute.fmt("{cdi:synthetic.toUpperCase}").render());
}
@Named("simple")
diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java
index 2e53fe166580d7..60797098731d9b 100644
--- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java
+++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java
@@ -5,20 +5,23 @@
import java.util.Set;
import java.util.function.Supplier;
+import io.quarkus.arc.Arc;
import io.quarkus.runtime.annotations.Recorder;
@Recorder
public class QuteRecorder {
- public Supplier