deep dive

for extension developers

 

Peter Palaga

@ppalaga

My main job:

Camel Quarkus

porting Camel componets to Quarkus

Agenda

  • Quarkus intro
  • AoT1 compilation: goodies and gotchas
  • How to write Quarkus extensions

  • 1) Ahead of (run-)time

What is Quarkus

Framework

Toolkit

for writing Java, Kotlin and Scala applications

Quarkus itself

Unopinionated

about how you should write your applications

Support for

Programming models

and

Frameworks

delegated to extensions

Why Quarkus?

Why Quarkus?

Container First

💾 Small size on disk Small container images
🚀 Fast boot time Instant scale up
🔬 Low memory footprint More containers with the same amount of RAM
Why Quarkus?

Developer joy

  • Easy to start with:
    mvn quarkus:create or code.quarkus.io
  • Live reload:
    mvn compile quarkus:dev
  • JUnit 5 extensions for testing
  • Isolation from GraalVM CLI/SPI
  • Maven or Gradle
  • Java, Kotlin or Scala
Why Quarkus?

Rich ecosystem of

Extensions

Why Quarkus?

Extensions coverage

Standards Databases 3rd party libs/frameworks
  • Java EE (JPA, CDI, JTA, ...)
  • MicroProfile (Health, Metrics, ...)
  • Spring (via compatibility layers)
  • PostgreSQL, MySQL
  • MSSQL, H2
  • FlyWay, Amazon DynamoDB
  • MongoDB, Neo4j
  • Netty, Vert.x
  • Camel, Infinispan, Kogito
  • ...
Why Quarkus?

Ecosystem

AoT1 compilation

 

its goodies and gotchas


1) Ahead of (run-)time

AoT compilation with GraalVM

 

the good parts
$ 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

The

gotchas

of the AoT compilation

An incomplete list

The

closed world

assumption

 

All runtime code has to be known at build time

 

  • More effective static analysis
  • Dead code elimination:
        classes, fields, methods, branches
unsupported

Dynaminc Classloading

in a native executable



Deloying jars, wars, etc. at runtime impossible

registration required

Reflection

Dynamic proxies

Classpath resources

JNI, Unsafe Memory Access

...

T ypically invoked only at compile time

Class initializers

1/3

Compiled to <clinit> method
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");
        ...
    }
}
Typically invoked only at compile time

Class initializers

2/3

At build time:

  • Resolve classes, run "safe" static initilizers
  • Take a snapshot of the produced heap
  • Store it in the executable
Typically invoked only at compile time

Class initializers

3/3

Downsides:

  • No file handles, sockets, threads
  • Autodetection of "safe" initializers not perfect
    • Manual tweaks required:
      --initialize-at-run-time

Complex CLI

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

How Quarkus works

Booting a framework

  • Many classes run only during the boot
  • Later unused
  • Still occupy memory
  • Parse config files: XML, YAML, JSON, ...
  • Classpath scanning, esp. for annotations
  • Build framework metamodel objects
  • Prepare reflection and build proxies
  • Open sockets, start threads
Quarkus:

Build time boot

As much work as possible done at build time

Output: recorded bootstrap bytecode

Build oriented container

How to write

Quarkus Extensions

Quarkus extensions


  • Units of Quarkus distribution
    • Maven deps of user apps
  • Configure, boot and integrate a lib/framework
  • Make the code lighter to run on a JVM
  • Make the code fit for the GraalVM

Two Maven modules

depends on
Build time module
my-ext-deployment
 
class MyExtProcessor {
  @BuildStep
  ... buildStep1(...) {...}

  @BuildStep
  ... buildStep2(...) {...}

  ...
}
Runtime module
my-ext
@Recorder
class MyExtRecorder {

  ... runtimeTask1(...) {...}


  ... runtimeTask2(...) {...}

  ...
}
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());
  }
}

Jandex

A Java annotation indexer
and offline reflection library.


Used by @BuildSteps
to inspect the application code


https://github.com/wildfly/jandex

A common @BuildStep:
 

  1. Find classes having some annotation
     
  2. Register them for reflection
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

More Jandex usecases

  • Find classes/fields/methods having some annotation
  • Find classes implementing an interface
  • Find subclasses of a class
  • List fields, methods and constructors of a class
Build time module
my-ext-deployment
 
class MyExtProcessor {
  @BuildStep
  ... buildStep1(...) {...}

  @BuildStep
  ... buildStep2(...) {...}

  ...
}
Runtime module
my-ext
@Recorder
class MyExtRecorder {

  ... runtimeTask1(...) {...}


  ... runtimeTask2(...) {...}

  ...
}
chunks of application bootstrap code
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

Extensions may depend on each other

E.g. mp-metrics extension
depends on
camel-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 */
}}

Mostly used BuildItems

  • ReflectiveClassBuildItem, ReflectiveHierarchyBuildItem
       register classes for reflection in the native mode
  • AdditionalBeanBuildItem, BeanDefiningAnnotationBuildItem
       bean classes the CDI container should analyze
  • ExtensionSslNativeSupportBuildItem
       turn on SSL in the native mode
  • CombinedIndexBuildItem, ApplicationIndexBuildItem
       offline reflection and annotation index (Jandex)
  • NativeImageResourceBuildItem
       src/main/resources and resources from JARs not included by default in the native image

Extension configuration

  • Extensions may expose their configuration through annotated POJOs
  • These may come in three flavors:
    • BUILD_TIME
    • BUILD_AND_RUN_TIME_FIXED
    • RUN_TIME

Config POJO

@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

Config in @BuildSteps

class CamelProcessor {
  @BuildStep
  ... discoverRoutesBuilderClassNames(CamelConfig config, ...) {
    if (config.main.enabled) {
        /* Do something */
    }
  }
}
                        
Code Adapted from the Apache Camel Quarkus

Config in @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

Substitutions

  • Provided by GraalVM
  • For replacing classes in the native image

Substitutions usecases

  • Last resort for fixing third party code
    • E.g. avoid opening a socket, starting a Thread, etc. in a static initializer
  • Cut off uneeded code (and its dependencies)
    • E.g. parsing an XML config that is not supported on Quarkus

A substitution example

(1/2)
/* 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

A substitution example

(2/2)
@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
Writing extensions

Wrap up

  1. Shrink and speedup the app
    • Inspect the app code (annotations, interfaces, config files ...) at build time
    • Bootstrap and configure the app via @Recorder methods
  2. Make the AoT compiler happy
    • Register reflection, proxies, JNI, ...
    • Resources to include in the native image
    • Substitutions