deep dive
for extension developers
Peter Palaga
porting Camel componets to Quarkus
1) Ahead of (run-)time
Framework
Quarkus itself
about how you should write your applications
Support for
and
delegated to extensions
💾 | Small size on disk | ✓ | Small container images |
🚀 | Fast boot time | ✓ | Instant scale up |
🔬 | Low memory footprint | ✓ | More containers with the same amount of RAM |
mvn quarkus:create
or code.quarkus.iomvn compile quarkus:dev
Rich ecosystem of
Standards | Databases | 3rd party libs/frameworks |
|
|
|
1) Ahead of (run-)time
$ native-image -jar my-app.jar
$ ls -lh
-rwxrwxr-x. 1 ppalaga ppalaga 19M Mar 20 14:39 my-app
$ ./my-app
...
my-app started in <20 ms
$ ps -o rss,command -p $(pgrep my-app)
PID RSS COMMAND
11229 12628 ./my-app
# ⮤ the process memory in kilobytes
All runtime code has to be known at build time
Deloying jars, wars, etc. at runtime impossible
Reflection
Dynamic proxies
Classpath resources
JNI, Unsafe Memory Access
...
1/3
class MyClass {
static Foo foo = new MyFoo();
static Map<String, String> map;
static {
System.out.println("Initializing MyFoo class...")
map = new HashMap<>();
map.put("k1", "v1");
map.put("k2", "v2");
...
}
}
2/3
At build time:
3/3
Downsides:
--initialize-at-run-time
Kept under the hood by Quarkus extensions
$ native-image -jar my-app.jar \
-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
-J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 \
-H:FallbackThreshold=0 \
-H:ReflectionConfigurationFiles=file-with-tens-of-entries.json
-H:+ReportExceptionStackTraces \
-H:+PrintAnalysisCallTree \
-H:-AddAllCharsets \
-H:EnableURLProtocols=http \
-H:-JNI \
-H:-UseServiceLoaderFeature \
-H:+StackTrace \
--no-server \
--initialize-at-build-time=my-build-time-init-list.json \
-J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-J-Dio.netty.leakDetection.level=DISABLED \
-J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
-J-Dsun.nio.ch.maxUpdateArraySize=100 \
-J-Dio.netty.allocator.maxOrder=1 \
-J-Dvertx.disableDnsResolver=true
|
|
As much work as possible done at build time
Output: recorded bootstrap bytecode
Build time module
my-ext-deployment
|
Runtime module
my-ext
|
class MyExtProcessor {
/* BuildSteps typically produce BuildItems */
@BuildStep
NameBuildItem nameStep() {
return new NameBuildItem("Joe");
}
/* BuildItems carry some payload */
final class NameBuildItem extends SimpleBuildItem {
final String name; /* The payload */
...
}
}
class MyExtProcessor {
/* BuildSteps may consume BuildItems */
@BuildStep
HelloBuildItem helloStep(NameBuildItem nameItem) {
return new HelloBuildItem("Hello " + nameItem.getName());
}
@BuildStep
NameBuildItem nameStep() {
return new NameBuildItem("Joe");
}
/* helloStep depending on the nameStep
* determines the execution order */
}
class MyExtProcessor {
@BuildStep
void helloStep(
NameBuildItem nameItem, /* BuildSteps may consume*/
GreetingBuildItem greetingItem, /* multiple BuildItems */
/* Results can also be published via BuildProducers */
BuildProducer<HelloBuildItem> helloProducer,
BuildProducer<FooBuildItem> fooProducer) {
/* BuildSteps may produce multiple BuildItems */
helloProducer.produce(
new HelloBuildItem(
greetingItem.getGreeting() + " " + nameItem.getName());
fooProducer.produce(new FooBuildItem());
}
}
A Java annotation indexer
and offline reflection library.
Used by @BuildSteps
to inspect the application code
@BuildStep
:class JacksonProcessor {
@BuildStep
void registerDeserializers(
CombinedIndexBuildItem combinedIndex, /* Pass the Jandex */
BuildProducer<ReflectiveHierarchyBuildItem> reflProducer) {
DotName JSON_DESERIALIZE =
DotName.createSimple(JsonDeserialize.class.getName());
for (AnnotationInstance annot /* Query for the annotations */
: combinedIndex.getIndex().getAnnotations(JSON_DESERIALIZE)) {
/* Do some filtering */
AnnotationTarget annotTarget = annot.target();
if (CLASS.equals(annotTarget.kind())) {
DotName dotName = annotTarget.asClass().name();
Type jandexType = Type.create(dotName, Type.Kind.CLASS);
/* ... and finally register the type for reflextion */
reflProducer.produce(new ReflectiveHierarchyBuildItem(jandexType));
}}}}
Code Adapted from the Jackson Extension
Build time module
my-ext-deployment
|
Runtime module
my-ext
|
class CamelProcessor {
@Record(ExecutionTime.STATIC_INIT) /* @Recorders can be injected */
@BuildStep /* ⮦ as BuildStep method params */
CamelContextBuildItem context(CamelRecorder recorder) {
RuntimeValue<CamelContext> context = recorder.createContext();
return new CamelContextBuildItem(context); /* ⮤ @BuildStep methods */
}/* ⮤ The RuntimeValue can be dispatched */ /* can invoke */
} /* to other @BuildSteps via a BuildItem */ /* @Recorder methods */
@Recorder
public class CamelRecorder {
public RuntimeValue<CamelContext> createContext() {
/* ⮤ A handle to pass values between recorders */
FastCamelContext context = new FastCamelContext();
return new RuntimeValue<>(context);
}}
Code Adapted from the Apache Camel Quarkus
E.g. mp-metrics
extension
depends oncamel-quarkus-core
so it can consume the CamelContextBuildItem
and adjust the CamelContext
as needed
class MpMetricsProcessor {
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
public void configureCamelContext(
MpMetricsRecorder recorder,
CamelContextBuildItem ctxItem) {
recorder.configureCamelContext(ctxItem.getCamelContext());
}}
@Recorder
public class MpMetricsRecorder {
public void configureCamelContext(RuntimeValue<CamelContext> ctxVal) {
/* Unwrap the RuntimeValue ⮧ */
CamelContext camelContext = ctxVal.getValue();
/* ... and configure it as needed */
ManagementStrategy strategy = camelContext.getManagementStrategy();
strategy.addEventNotifier(new MpMetricsCamelContextEventNotifier());
}}
Code Adapted from the Apache Camel Quarkus
Resulting application bootstrap code:
class GeneratedMain { /* bytecode in reality, pseudocode here */
static { /* Chunks recorded with @Record(ExecutionTime.STATIC_INIT) */
CamelRecorder camelRecorder = new CamelRecorder();
RuntimeValue<CamelContext> val1 = camelRecorder.createContext();
/* The order of the calls is given by
* the dependencies between the @BuildSteps */
MpMetricsRecorder mpMetricsRecorder = new MpMetricsRecorder();
mpMetricsRecorder.configureCamelContext(val1);
}
public static main(String[] args) {
/* Chunks recorded with @Record(ExecutionTime.RUNTIME_INIT) */
new VertxRecorder().openSocket(); /* can't do this in static init */
}}
BuildItems
ReflectiveClassBuildItem
, ReflectiveHierarchyBuildItem
AdditionalBeanBuildItem
, BeanDefiningAnnotationBuildItem
ExtensionSslNativeSupportBuildItem
CombinedIndexBuildItem
, ApplicationIndexBuildItem
NativeImageResourceBuildItem
BUILD_TIME
BUILD_AND_RUN_TIME_FIXED
RUN_TIME
@ConfigRoot(name = "camel", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class CamelConfig {
@ConfigItem
public MainConfig main;
@ConfigGroup
public static class MainConfig {
@ConfigItem(defaultValue = "true")
public boolean enabled;
/* This option can be set in application.properties via
*
* camel.main.enabled = true
*
*/
}
}
Code Adapted from the Apache Camel Quarkus
@BuildSteps
class CamelProcessor {
@BuildStep
... discoverRoutesBuilderClassNames(CamelConfig config, ...) {
if (config.main.enabled) {
/* Do something */
}
}
}
Code Adapted from the Apache Camel Quarkus
@Recorders
@Recorder
public class MyRecorder {
void record(CamelConfig camelConfig) {
if (camelConfig.main.enabled) {
/* Do something */
}
/* Same effect using
* org.eclipse.microprofile.config.ConfigProvider */
if (ConfigProvider.getConfig()
.getValue("camel.main.enabled", Boolean.class)) {
/* Do something */
}
}}
Code Adapted from the Apache Camel Quarkus
/* The original class (simplified) */
package com.mysql.cj.jdbc;
public class AbandonedConnectionCleanupThread implements Runnable {
private static final ExecutorService cleanupThreadExcecutorService;
private static Thread threadRef = null;
static {
cleanupThreadExcecutorService =
Executors.newSingleThreadExecutor(r -> { ... });
/* GraalVM does not like threads being started in a static block */
cleanupThreadExcecutorService.execute(
new AbandonedConnectionCleanupThread());
}
...
}
Code Adapted from the MySQL JDBC driver
@Substitute /* The substitution class */
@TargetClass(AbandonedConnectionCleanupThread.class)
final public class AbandonedConnectionCleanupThread_disable {
/* No static initializer here */
@Substitute
protected static void trackConnection(
MysqlConnection conn, NetworkResources io) {
/* A no-op method */
}
}
Code Adapted from the MySQL JDBC extension