Writing

Extensions

 

Peter Palaga

@ppalaga

 

My main job:

Camel Quarkus

porting Camel componets to Quarkus

mvnd - the Maven Daemon

https://github.com/mvndaemon/mvnd

Twitter

Agenda

  • How Quarkus works
    • Build time augmentation
    • Native compilation with GraalVM
  • How to write Quarkus extensions

What is Quarkus

Framework

Build time augmentation

Toolkit

Quarkus itself

Unopinionated

about how you should write your applications

Support for

Programming models

and

Frameworks

delegated to extensions

How Quarkus works

Quarkus workflow
Quarkus workflow

Booting a framework

  • Parse config files: XML, YAML, JSON, ...
  • Classpath scanning, esp. for annotations
  • Build framework metamodel objects
  • Prepare reflection and build proxies
  • Open sockets, start threads
  • ← Takes time
  • ← Spends memory

Augmentation

Do as much as possible at build time

Record the code needed to bootstrap the application

Quarkus workflow
Quarkus workflow

AoT1 native 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/config

Kept under the hood by Quarkus extensions

$ native-image -jar my-app.jar \
    -H:ReflectionConfigurationFiles=file-with-tens-of-entries.json \
    -H:ResourceConfigurationFiles=maintain-this.json \
    -H:DynamicProxyConfigurationFiles=maintain-this.json \
    -H:JNIConfigurationFiles=maintain-this.json \
    -H:SerializationConfigurationFiles=maintain-this.json \
    --initialize-at-build-time=my-build-time-init-list.json \
    -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
    -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 \
    -H:FallbackThreshold=0 \
    -H:+ReportExceptionStackTraces \
    -H:+PrintAnalysisCallTree \
    -H:-AddAllCharsets \
    -H:EnableURLProtocols=http \
    -H:-JNI \
    -H:-UseServiceLoaderFeature \
    -H:+StackTrace \
    --no-server \
    -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 to write

Quarkus Extensions

Quarkus extension


  • Unit of Quarkus distribution
    • A Maven dep of a user app
  • Focus on some specific lib/framework/aspect

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

A common @BuildStep:
 

  1. Find classes having some annotation
     
  2. Register them for reflection

Jandex

A Java annotation indexer
and offline reflection library.


Used by @BuildSteps
to inspect the application code


https://github.com/wildfly/jandex
class JacksonProcessor {
  @BuildStep
  void registerDeserializers(
        CombinedIndexBuildItem combinedIndex, /* Pass the Jandex */
        BuildProducer<ReflectiveClassBuildItem> 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 ReflectiveClassBuildItem(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
       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

List of BuildItems provided by Quarkus

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
         *
         *   quarkus.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("quarkus.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

Further topics

Where to look for examples

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 native compiler happy
    • Register reflection, proxies, JNI, ...
    • Resources to include in the native image
    • Substitutions