Skip to content

Continual Assembly

Overview

The Assembly system is Continual’s low-level framework for building processes from a component specification.

In general, our design strategy involves creating service interfaces that are stitched together explicitly in the process. This gets us most of the decoupling we want in the system but leaves out dependency injection magic. Our service’s constructors receive a service container and a configuration object. They’ll then ask the service container for all dependencies by name. (Generally that name comes from the configuration passed to the service constructor.)

Services and the Service Container

In the Assembly framework, a Service is a Java interface that requires a class to start, stop, and return current run status.

Assembly’s top-level object is the ServiceContainer. This object holds some number of named Service instances. The ServiceContainer can be started and stopped. On start, the ServiceContainer starts its contained services. On stop, the ServiceContainer signals each service to stop. A caller can run awaitTermination() to wait for all services in the container to report that they’ve stopped.

During startup, each Service is presented with a reference to the ServiceContainer as well as its own specific configuration data. The service constructor can therefore find the other services it needs to run. For example, an HTTP API service component may want to find an IdentityManagement service by name.

💡 Assembly doesn’t allow for a component search by interface. Objects are always named and explicitly connected that way.

Configuration

The Assembly framework allows you to configure your system using a JSON document that specifies a list of service components, additional configuration information, and runtime profiles. The ServiceContainer class knows how to read this document, typically called services.json, to build a container instance.

Services

The most important segment of the JSON configuration is the services array. Each object within this array represents a Service instance with the following fields:

FieldDescription
classThe class used to construct the service instance.
nameThe instance’s name in the service container.
enabledIf false, the service object is ignored. Defaults to true.

The entire JSON object is provided to the class constructor allowing you to make additional settings for the service instance.

{
"services":
[
{
"name": "emailer",
"class": "io.continual.email.impl.SimpleEmailService",
"mailSmtpServer": "smtp.gmail.com",
"mailLogin": "[email protected]",
"mailPassword": "mypassword",
"mailFromEmail": "[email protected]",
"mailFromName": "John Doe"
}
]
}

Evaluation

Depending on the service component implementation, most configuration fields can be defined using references to other data including the environment and the Java system command line.

For example, to use an environment variable for the SMTP server in the example above:

{
"services":
[
{
"name": "emailer",
"class": "io.continual.email.impl.SimpleEmailService",
"mailSmtpServer": "${SMTP_SERVER}",
"mailLogin": "[email protected]",
"mailPassword": "mypassword",
"mailFromEmail": "[email protected]",
"mailFromName": "John Doe"
}
]
}

Config

Another top-level object in the services configuration is the config object. Data stored in this object can be referred to in service configurations:

{
"config":
{
"foosvc":
{
"host":"foo.example.com",
"port":8080
}
},
"services":
[
{
"name": "FooUser",
"class": "com.example.FooUser",
"server":"${foosvc.host}:${foosvc.port}"
}
]
}

Profiles

Finally, the top-level profiles object can be used to select among configurations at startup. Each sub-object of an active profile is merged over the service it names. For example, suppose we want to disable the FooUser service when the system starts with profile “noFoo” active:

{
"config":
{
"foosvc":
{
"host":"foo.example.com",
"port":8080
}
},
"services":
[
{
"name": "FooUser",
"class": "com.example.FooUser",
"server":"${foosvc.host}:${foosvc.port}"
}
],
"profiles":
{
"noFoo":
{
"FooUser":
{
"enabled": false
}
}
}
}

Running a Service Container System

Applications can use the Assembly framework in a variety of ways, but the most typical use, at least for Continual-built systems, is to use a main class that subclasses io.continual.services.Server and passes main()‘s arguments to runFromMain().

For systems built this way, command line arguments are as follows:

  • -s: specify a services JSON file. Defaults to services.json and is expected to be found somewhere in the CLASSPATH
  • -p: specify a comma-separated list of profiles to activate. If this argument is not included, default becomes active if it exists in the configuration.

When the system starts, the services file is processed, all enabled service components are instantiated, and then all enabled service components are started.

The process continues to run until all services exit. If a termination signal is received (e.g. SIGTERM), the service process requests that all service components terminate.

Building a Service Component

The Assembly framework includes a Builder utility that constructs objects given a class name and configuration data. Once the builder locates the class specified in the configuration, it looks for a suitable constructor. The following methods are considered, in order:

A static initializer method that expects a JSON object of config data:

public static SomeClassName fromJson ( JSONObject data );

A non-static initializer after constructing with a no-arg constructor:

public void fromJson ( JSONObject data );

A static initializer method that expects data as well as the service container:

public static SomeClassName fromJson ( JSONObject data, ServiceContainer svcs );

A non-static initializer after constructing with a no-arg constructor, this time expecting data as well as the service container:

public void fromJson ( JSONObject data, ServiceContainer svcs );

A constructor that expects data as well as the service container:

public SomeClassName ( ServiceContainer svcs, JSONObject data );

A constructor that expects just the data class:

public SomeClassName ( JSONObject data );