• 510.205.3525

  • hello at getvariant.com

Variant Experimentation Server Extension SPI User Guide

Release 1.4.0, September 2024

1 Overview

Variant server’s functionality can be extended through the use of the server-side extension service programming interface (SPI), which enables user-defined code to be directly executed by the server process. 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 are supported:

  • Lifecycle Hooks are handlers for various lifecycle events, such as when a session is about to be qualified for an experiment. A user-defined handler can implement a custom qualification logic, e.g. checking if the user is already registered, if, e.g. a feature flag is only open to unregistered users.
  • Trace Event Flushers handle the egest of trace events into external storage, like a database or an event queue. Each experiment schema can have its own event flusher.

Both the hooks and the flushers are configured in the experiment 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 Lifecycle Events and Hook Scope

Lifecycle hooks provide callback methods which are posted by Variant server whenever a lifecycle event of interest is raised. A lifecycle hook listens to a particular lifecycle event type by implementing the post() method of an appropriate interface:

HookListens to
QualificationLifecycleHookQualificationLifecycleEvent
TargetingLifecycleHookTargetingLifecycleEvent

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

  • Schema-scoped hooks are defined at the root level. These hooks are applicable to all states and all experiments in the schema.
  • State-scoped hooks are defined with their state definitions. These hooks are applicable to the containing state only.
  • Experiment-scoped hooks are defined inside with their experiment definitions. These hooks are applicable to the containing experiment only.

QualificationLifecycleEvent is raised when a Variant session must be qualified for a experiment. Posts all eligible schema-scoped and experiment-scoped hooks whose experiment matches that of triggering event. Used to qualify (or disqualify) a user session for the triggering experiment, based on a custom qualification algorithm. If no custom qualification hooks were provided in the schema, the default qualification algorithm qualifies all session into all experiments.

TargetingLifecycleEvent is raised when a Variant session must be targeted for a experiment. Posts all eligible schema-scoped, state-scoped hooks whose state and state match that of triggering event, and those experiment-scoped hooks whose experiment matches that of the triggering event. Used to provide custom targeting algorithm. If no custom targeting hooks were provided in the schema, the default targeting algorithm targets randomly and uniformly between all the given experiment’s experiences defined on the the given state.

2.2. Schema Definition and Instantiation

The schema definition of a hook in any scope has the following three components

KeyTypeRequiredCommentDefault
classany-stringYesThe fully qualified name of the Java class implementing the hook.
namename-stringNoThis hook’s name.The simple (unqualified) class name.
initfragmentNoAny YAML value.None

Example:

name: minimal_schema
...
hooks:
  - class: mycompany.variant.spi.RecaptchaQualificationHook
    init: [USERID1 USERID2 USERID3]

The implementing class must be added to the server’s classpath to be discoverable. The way to accomplish that is to copy the compiled JAR files and their dependencies to the server’s /spi directory at the server startup time.

By contract, an implementation must define at least one of these constructors:

  • If no init property is given in the hook definition, Variant server will first look for a nullary constructor. If absent, Variant server will look for a constructor that takes a single parameter of type YamlNode<T> and invoke it with the null value.
  • If an init property is given, Variant server will look for a constructor that takes a single parameter of type YamlNode<T> and pass it the parsed YAML value.

Variant server creates a single class instance for each hook definition in the schema and reuses them for all eligible events in a highly-multithreaded environment. The application programmer must ensure that

  • The hook class has no mutable state.
  • The hooks’ post() method is thread safe.

2.3. Runtime 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:

  • Experiment-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, within each 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:

  • The default qualification algorithm qualifies all session into all experiments.
  • The default targeting algorithm targets randomly and uniformly between all of the given experiment’s experiences defined on the the given state.

3 Trace Event Flushers

Trace events are generated by user sessions, as they navigate the host application’s state graph. Event flushers handle the egest of these events to some external storage with the purpose of subsequent analysis. 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.

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 multi-threaded event writer per Variant server, shared by all schemas. Event writer groups trace events by the schema which produced them and turns them over to that schema’s event flusher by calling its flush() method.

Each call to the flush() method is scheduled to be executed asynchronously and 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.

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

name: example_schema
flusher:
  class: com.variant.spi.stdlib.flush.TraceEventFlusherCsv
  init:
    header: true
    file: /tmp/variant-events.csv
# ...

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

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

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

4 Developing for the Server-Side Extension SPI

Download the server side extension SPI library:

% curl -O https://s3.us-west-1.amazonaws.com/com.variant.pub/1.3.1/variant-spi-1.3.1.jar

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 a 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) until the server is restarted. Always restart your Variant server when you redeploy your custom SPI classes.

5 The Standard Extension Library

TBD

Index