Press "Enter" to skip to content

Variant CVM Server-Side Extension SPI User Guide

Release 1.0.1 December 2023

1 Overview

Variant server’s functionality can be extended through the use of the server-side extension SPI, which enables user-defined operation to be executed by the server. The server-side SPI exposes Java bindings which facilitate injection of custom semantics into the server’s default execution path via an event subscription mechanism. Two types of user-defined event handlers objects are supported:

  • Lifecycle Hooks are handlers for various lifecycle events, such as when a session is about to be qualified for a variation. A user-defined handler can implement a custom qualification logic, e.g. checking if the current application user is already registered.
  • Trace Event Flushers handle the egestion of trace events into an outboard storage, like a database or an event queue. Each variation schema gets its own event flusher.

Both the hooks and the flushers are configured in the variation schema. The next two chapters explain their semantics and configuration, and [todo] chapter 4.3 provides details on how to write them.

2 Lifecycle Hooks

2.1 Instantiation and Scope

Lifecycle hooks provide callback methods which are posted by Variant server whenever a lifecycle event of interest is raised. A lifecycle hook subscribes to a particular lifecycle event type by implementing the LifecycleHook.gerLifecycleEventClass()  method. Whenever an event of that type is raised, the hook is notified by Variant server via its callback method LifecycleHook.post() .

Hook definitions may appear in variation schema in any of three scopes:

  • Schema-scoped hooks are defined at the root level. These hooks are applicable to all states and all variations in the schema. Hooks listening to lifecycle events descendant from StateAwareLifecycleEvent  will be posted for each state in the schema by every state aware lifecycle event. Hooks listening to lifecycle events descendant from VariationAwareLifecycleEvent  will be posted for all online variations in the schema by every variation aware lifecycle event.
  • State-scoped hooks are defined inside the state definition. These hooks are applicable to the enclosing state only and must listen to events descendant from StateAwareLifecycleEvent .
  • Variation-scoped hooks are defined inside the variation definition. These hooks are applicable to the enclosing variation only and must listen to events descendant from VariationAwareLifecycleEvent .

2.2 Lifecycle Events

The following is the complete list of lifecycle events raised by Variant server:

Lifecycle EventEvent ScopeDefault Hook
VariationQualificationLifecycleEvent Schema VariationSession is qualified for the variation.
Raised when a Variant session must be qualified for a variation. Posts all eligible schema-scoped hooks and those variation-scoped hooks whose variation matches that of triggering event. Used to qualify (or disqualify) a user session for the triggering variation, based on a custom qualification criteria.
VariationTargetingLifecycleEvent Schema State VariationVariation is targeted randomly, according to the weight properties.
Raised when a Variant session must be targeted for a variation. Posts all eligible schema-scoped hooks, those state-scoped hooks whose state matches that of triggering event, and those variation-scoped hooks whose variation matches that of the triggering event. Used to provide custom targeting algorithm.

2.3 Hook Chaining

In any scope, any number of hooks can be defined. If more than one lifecycle hook is eligible to be posted by a lifecycle event at runtime, they form a hook chain.

A hook chain is posted in the following order:

  • Variation-scoped hooks, then state-scoped hooks, then schema-scoped hooks.
  • Within a scope, hooks are posted in the ordinal order, i.e. the order in which they are defined in the schema, in the corresponding scope.

The hooks are posted serially, until a hook’s post() method returns a non-empty Optional. If no custom hooks have been defined for a lifecycle event, or all returned an empty Optional, the default built-in hook for the event is posted, which is guaranteed to return a usable value.

A hook chain is posted synchronously; the hooks’ post() methods are invoked one at a time while the foreground user session thread is blocked. Conequently, the post() method need not be thread safe. A new instance of a hook is instantiated for each invocation of the post() method.

When a hook is posted, its post(E event)  method is called by Variant server with the actual triggering lifecycle event instance. If the post(E event) method returns a non-null value, Variant server ignores the rest of the hook chain, expecting the returned object to contain the information it requires to proceed. Otherwise, Variant posts the next hook on the chain.

2.4 Custom Lifecycle Hooks

A custom lifecycle hook must implement the LifecycleHook  interface. By contract, an implementation must also provide at least one of these constructors:

  • Nullary constructor, if no init property was given in the hook definition.
  • Single argument constructor with argument type Config . If no init property was given and no nullary constructor is available, this constructor will be called with null argument; otherwise, the value of the init property will be parsed and passed to this constructor.

Refer to Section 4.3 for packaging details.

3 Trace Event Flushers

Trace events are generated by user traffic, as it flows through Variant variations. Event flushers handle the terminal ingestion of these events with the purpose of subsequent analysis by a downstream process. Trace events can be triggered implicitly, by Variant, or explicitly by the host application. In either case, the host application can attach attributes to these events, to aid in the downstream analysis.

Variant server automatically enriches all trace events with the following metadata:

  • Variant session ID by which related events can be associated.
  • Names of sessions’s live experiences.
  • Custom event attributes.

A typical event flusher writes trace events to a persistent storage mechanism, such as an external database or event stream. Whenever a trace event is triggered — implicitly by Variant server or explicitly by user code — it is picked up by the Variant’s asynchronous event writer, where it is held in a memory buffer until a dedicated flusher thread becomes available. There is one event writer per Variant server, shared by all schemata. Event writer groups trace events by the schema that produced them and turns them over to the apropriate event flusher by calling its flush() method.

Each call to the flush() method is scheduled to be executed asynchronously and, potentially concurrently with other similar calls, by a dedicated thread pool, whose size is configured by the variant.event.writer.buffer.flush.pool.size config property. Buffers are passed to the thread pool as soon as they fill up or the variant.event.writer.max.delay number of seconds has elapsed since the time when the oldest event in the buffer was triggered.

Note, that it is possible that the same flusher instance is accessed by multiple threads, so it is critical that the flush() method you write be thread safe. In particular, avoid mutable instance state in custom event flushers.

Trace event flushers are configured at the root of the variation schema, as in the following example:

name: my_schema
flusher:
  class: com.variant.spi.stdlib.flush.TraceEventFlusherCsv
  init: "{'header':true, 'file':'/tmp/variant-trace-events-petclinic.csv'}"
states:
    # ...
variations:
    # ...
}

If no event flusher is configured in the schema, the system wide default is assumed, as configured by the variant.event.flusher.class.* properties.

A number of pre-built trace event flushers come with the server, as part of the standard extension library, discussed in Section 4.4.

4 Developing for the Server-Side Extension SPI

Variant server-side SPI library variant-server-spi-<release>.jar can be downloaded from the Variant website. Create a new Java project in your IDE, add this JAR file to the classpath and you are set. You must package your project as JAR. To add the packaged JAR file to Variant server’s runtime classpath, copy it (and its dependencies) into the server’s spi/ directory.

Alternatively, but with the same ultimate result, — you may clone the standard extension public repository  into a local workspace, remove the source files, change the groupId, artifactId and the version  to suit your environment,— and you have a working shell of a brand new Variant SPI development project.

Note, that a running Variant server loads all hook and flusher classes from the class path only once, when they are first encountered. Therefore, replacing classes in the server’s spi/ directory will not have any effect and may even lead to unexpected behavior. Always restart your Variant server when you redeploy your custom SPI classes.

5 The Standard Extension Library

Variant server standard extension is a library of general purpose extensions, written on top of the extension SPI. They provide out-of-the-box functionality which is not part of the core Variant server. They are packaged as variant-server-spi-stdlib-<release>.jar file wich is incliuded in the server distribution in the spi/ directory.

The standard extension library is an open source project, available on GitHub  under the Apache 2 license. You may find it useful to examine the source code, before developing your own custom lifecyle event hooks and trace event flushers.

Package com.variant.spi.stdlib.fush contains the following ready to use trace event flushers:

TraceEventFlusherNull 
Discards all trace events. Configuration: None. Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.TraceEventFlusherNull’ }
TraceEventFlusherServerLog 
Appends trace events to the application logger. This is the default, out of the box event flusher, which is completely independent of the operational environment. Probably not for production use. Configuration: level – specifies the logging level to be used. Defaults to ‘INFO’. Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.TraceEventFlusherApplicationLog’, ‘init’:{‘init’:”info”} }
TraceEventFlusherCsv 
Writes trace events to a local CSV file. The output file format conforms to the IETF RFC4180 specification. Configuration: header – boolean – Wether or not to include the metadata header as very first line. The default is false. file – string – The name of the file to write to. Will be overwritten if exists. The default is “variant-events.csv” Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.TraceEventFlusherCsv’, ‘init’:{‘file’:’/tmp/variant-events.csv’,’header’:true} }
jdbc/TraceEventFlusherH2 
Writes Variant events to an H2 database. The SQL scripts required to create the database schema expected by this flusher can be found in db/h2  directory. You must also copy the H2 JDBC driver into Variant server’s spi/ directory. Configuration: url – string – The URL to the H2 database instance. user – string – The database user name. password – string – The database user’s password. Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.jdbc.TraceEventFlusherH2′, ‘init’:{ ‘url’:”jdbc:h2:mem:variant;MVCC=true;DB_CLOSE_DELAY=-1;’, ‘user’:’variant’, ‘password”:’variant’} }
jdbc/TraceEventFlusherMysql 
Writes Variant events to a MySQL database. The SQL scripts required to create the database schema expected by this flusher can be found in db/mysql  directory. You must also copy the MySQL JDBC driver into Variant server’s spi/ directory. Configuration: url – string – The URL to the MySQL database instance. user – string – The database user name. password – string – The database user’s password. Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.jdbc.TraceEventFlusherMysql’, ‘init’:{ ‘url’:”jdbc:mysql://localhost/variant’, ‘user’:’variant’, ‘password”:’variant’} }
jdbc/TraceEventFlusherPostgres 
Writes Variant events to a PostgreSQL database. The SQL scripts required to create the database schema expected by this flusher can be found in db/postgres  directory. You must also copy the Postgres JDBC driver into Variant server’s spi/ directory. Configuration: url – string – The URL to the PostgreSQL database instance. user – string – The database user name. password – string – The database user’s password. Example:‘flusher’: { ‘class’:’com.variant.spi.stdlib.flush.jdbc.TraceEventFlusherPostgres’, ‘init’:{ ‘url’:”jdbc:postgresql://localhost/variant’, ‘user’:’variant’, ‘password”:’variant’} }