Live fast, die young and leave a good-looking corpse

Any application that keeps state needs a plan for maintaining it as the application evolves. Successfully managing these upgrades is one of the more challenging aspects of application development. This document explains several patterns and techniques for managing upgrade in a Waterken application.

Persistence engine

Application objects hosted by the Waterken server can exploit the provided orthogonal persistence. At the end of every event loop turn, before any externally visible I/O is done, the Waterken server persists all application objects that are reachable from previously persisted objects. This graph of persistent objects is rooted in objects bound to an exported web-key. The programmer expresses different strategies for persistent state through patterns of extending, modifying and using this persistent object graph. This paper presents these patterns and shows how they can be expressed using the ref_send API.

Ephemeral pleasures

An effective strategy for dealing with the challenges of persistent state is avoiding it. When using the Waterken server, there are several ways in which persistence can be usefully avoided.

Any created Java object that is not referenced by a previously persisted Java object at the end of an event loop turn is not persisted. Since the object is never stored, the programmer is freed from the burden of planning for its upgrade.

The GUI for a Waterken application is often a web-application. The state of this GUI is rebuilt anew on each visit to the application, using state pulled into the browser via web-keys. It's a good strategy to express as much of the application logic as possible in the ephemeral web-application, rather than in persistent Java objects. Think of the state maintained by the Waterken server as the application's model, as in the Model-View-Controller (MVC) pattern. Your application's view and controller are then implemented in the ephemeral web-application. This design reduces the upgrade challenge to the design of the application's model. Constraining the scope of the upgrade challenge in this way is the most important step to take in the design of an application's persistence strategy.

Minor surgery

As an application evolves, its model may need to be extended. Objects are persisted by the Waterken server using Java Object Serialization, which supports a useful set of compatible upgrades, such as:

When upgrading an object requires an algorithmic step, a class can declare a readObject method of the form:

private void
readObject(final ObjectInputStream in) throws IOException,
                                              ClassNotFoundException {
    // read in the old state
    in.defaultReadObject();

    // using the old state, upgrade the object to the new format
    …
}

Java Object Serialization only allows these upgrades if the serialized class declares them to be allowed. This is done by declaring a serialVersionUID field, such as:

    static private final long serialVersionUID = 1L;

This field should be declared as part of the first steps in creating a persistent class.

Pushing all state through a minor upgrade

Once the desired changes have been made to a set of upgraded classes, the upgrade can be applied using the touch.jar command provided by the Waterken server. For example, to upgrade a vat named test, as well as all of its child vats:

  1. stop the Waterken server
  2. replace the old class files with the upgraded ones
  3. from the home folder of the Waterken installation, run the command:

    java -jar touch.jar vat/test

  4. restart the Waterken server

The touch.jar command sweeps through all of the persistent state for the named vat, pushing each object through the Java Object Serialization upgrade process and saving its new state to disk. The command runs as a normal vat transaction and so will either commit with all objects upgraded, or abort, leaving the vat in its original state.

Amputation

Sometimes, it's not feasible to express an algorithm for upgrading old state using the techniques described in the previous section. In this case, it's also sometimes the case that the old state is no longer needed in the upgraded application. In this situation, a viable upgrade strategy is to simply plan to abandon some objects. When the Waterken server is deserializing part of an object graph, it doesn't immediately follow an eventual reference, but instead delays loading of this part of the graph until the target object is accessed by the application. To plan for amputation of part of an application's persistent object graph, only refer to objects in that part of the graph using eventual references. Once bit rot takes hold in that part of the object graph, change the application code to no longer access it. If the stale objects are no longer accessed, they will never be loaded and so will never produce incompatible serialization errors.

Commuter pattern

To support delayed loading of part of an object graph, and so enable amputation of it, a class must use an eventual reference to refer to an object that instances may need immediate access to. To facilitate this pattern, the ref_send API provides some syntactic sugar:

import static org.ref_send.promise.eventual.Eventual.near;
…

public final class
Stable implements Serializable {
    static private final long serialVersionUID = 1L;

    private final Eventual _;
    private       Unstable dependency_;

    …

    public void
    setDependency(final Unstable dependency) {
        dependency_ = _._(dependency);
    }

    public void
    doSomething() {
        final Unstable dependency = near(dependency_);
        final String foo = dependency.getFoo();
        …
    }
}

If the type Unstable in the example above is a class, rather than an interface, implementation of the pattern is slightly different:

import static org.ref_send.promise.Fulfilled.detach;
import static org.ref_send.promise.eventual.Eventual.near;
…

public final class
Stable implements Serializable {
    static private final long serialVersionUID = 1L;

    private final Eventual _;
    private       Promise<Unstable> dependency_;

    …

    public void
    setDependency(final Unstable dependency) {
        dependency_ = detach(dependency);
    }

    public void
    doSomething() {
        final Unstable dependency = near(dependency_);
        final String foo = dependency.getFoo();
        …
    }
}

Implants

There are two major roles for an application's model: storing information and enforcing the rules that govern changes and access to this information. Evolving an application sometimes means supporting new ways of manipulating the same underlying information. To support this kind of upgrade, an application's model should segregate objects that hold the application's state from objects that define the application's behaviour. The goal of this segregation is to enable creation of a new layer of behaviour objects for the same set of state objects.

For example, the yurl.net DNS nameserver is a Waterken server application for managing DNS records. When the genkey.jar command is run, a new vat is created to manage the DNS resources for a newly created hostname. The application model stored in this vat segregates application state, the held DNS resources, from application behaviour, editing of those resources, and provides a hook for the introduction of new behaviour.

The nameserver's application state is held in an array of variables, each of which holds an arbitrary Resource.

The nameserver's behaviour is defined by the ResourceGuard, which constrains the supported DNS resource types, and the Menu, which supports editing of the variable array.

On creation, a DomainMaster object provides the client access to objects that define the application's current behaviour, as well as an object that may define future application behaviour. The extension member of DomainMaster refers to an object that holds references to all of the objects embodying the application's state. This extension object provides a hook for introducing new behaviour for the existing application state. The hook is used by adding a new method to the extension object that creates a new layer of behaviour objects.

Offload

If plans for upgrade don't work out, or the cumulative complexity from multiple upgrades becomes too much, it may be best to start over with a new design, bootstrapping new vats with application data pulled in from the old model. To enable this offloading of application state, the application model should support a query interface that can generate documents describing the current application state.

For example, the previously discussed DNS nameserver supports creation of such application state documents via the Menu's getSnapshot() method. This method returns a document describing all of the current DNS resources for a hostname.

Application design philosophy

In addition to its role in persistence, the vat is also the unit of concurrency. Each vat is serviced by its own thread, so spawning a new vat creates a new thread, enabling concurrent execution within an application. To exploit multiple processor cores, an application should be designed to use many vats.

Once an object is persisted, its state remains on disk until the hosting vat is destructed. If the object is not in use, it will never be loaded and so won't consume the computer's processing power or memory, but will consume filesystem space. To reclaim this space, an application should be designed around short-lived vats that are periodically destructed, giving way to new vats. Whenever an application is about to engage in an activity that will generate a bunch of new persistent objects, a new vat should be spawned for that activity. When the activity completes, the spawned vat should be destructed, after producing a document describing the outcome of the activity.

To best leverage the platform provided by the Waterken server, applications should be designed around a vat usage philosophy of: "Live fast, die young and leave a good-looking corpse". Spawn vats frequently to make an application faster by exploiting concurrency. Destruct vats soon to avoid the upgrade problems that come with long-lived state and to reclaim filesystem space. Produce state description documents to facilitate upgrade and interoperation with other applications.