Baratine on GitHub

Vault Services

../../_images/baratine-vault.png

A vault is a collections of assets saved in a database. A book vault’s assets would be its books. Vault assets operate in-memory using the vault’s thread and inbox.

Overview

Assets are the in-memory model for persistent objects.

  • Assets are encapsulated. Application code operates on fields without locking or transactions.
  • Assets need a single writer to ensure atomic updates.
  • The single writer is a Baratine service.
  • Because of the single owning thread, asset services must be non-blocking for performance.

Book Vault

A book vault has an vault interface and a book asset implementation. The vault and its assets are a single Baratine Service.

The vault creates, finds and deletes assets. It acts across all assets in the vault.

The asset contains application logic, acting on the asset as an encapsulated object. Internally, asset loads and saves itself, because of the single-writer principle. Single-writer is required to support atomic and consistent updates.

Application logic operates on the asset as an in-memory encapsulated object. Methods that update the data need a @Modify annotation to tell the asset to save its state.

IdAsset is a built-in 64-bit identifier, which displays as a base-64 string, and is designed to be unique across a Baratine cluster. (IdAsset resembles the instagram id model.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import io.baratine.service.*;
import io.baratine.vault.*;

@Service
public interface BookVault extends Vault<IdAsset,Book>
{
  void create(Book initialData, Result<IdAsset> generatedId);

  void findByTitle(String title, Result<Book> result);

  void delete(IdAsset id);
}

@Asset
public class Book
{
  private @Id IdAsset id;
  private String title;

  public void get(Result<Book> result)
  {
    result.okShim(this);
  }
}

The get methods returns the book’s data through a okShim() call to preserve encapsulation. Shims use transfer objects to return data to a caller to keep the asset’s active data private to the asset. The shim uses the Book object as a transfer object without forcing the application to write tedious copy and clone code.

In general, assets and services should use coarse-grained calls like the get() above instead of fine-grained calls like getTitle(), because an asset might be partitioned and distributed across multiple servers, and to encourage an event-sourcing model of programming.

Note

For a create call, the new asset saves the created data itself, not the vault, because of the single-writer principle. In a create call the vault generates the new id and the asset to create itself. Similarly, a delete call asks the asset to delete itself.

Asset Persistence - Singleton

An asset service is a persistent service marked by the @Asset annotation.

  • Asset services are object-oriented; applications use in-memory field data.
  • Fields are stored in Baratine’s document database.
  • Each service owns its own data.
  • @Modify marks updating methods to save to the database.

Any singleton service can become persistent by using the @Asset annotation. The loading and saving is the same as for a vault of assets. Singleton services don’t have create, delete or find methods because there’s only one object.

Assets work on their data in-memory, as an encapsulated object. Their data is loaded when the service initializes, and the data is saved when a @Modify method is called. The data is only loaded once because the service owns its data. Because no other service can modify the data, the data can’t become invalid.

The asset is non-locking and non-blocking. Saves occur when the asset is idle, which improves batching under heavy load.

Assets serialize their data to the internal kelp database, using H3 Serialization for serialization. H3 Serialization is a binary format that supports object queries, which is used for find methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import io.baratine.service.Result;
import io.baratine.service.Service;
import io.baratine.service.Modify;
import io.baratine.vault.Asset;

@Asset
@Service
public class Hello
{
  private String _greeting = "generic greeting";

  public void getGreeting(Result<String> result)
  {
    result.ok(_greeting);
  }

  @Modify
  public void setGreeting(String newGreeting)
  {
    _greeting = newGreeting;
  }
}

Line notes:

    1. @Asset marks a service as a persistent asset.
    1. Assets are always services because the service lifecycle and threading is essential to Baratine’s persistence.
    1. Assets use fields without locking because the service manages the single thread. In-memory is the standard operation mode.
    1. Async Result is needed because the service must be non-blocking.
    1. @Modify marks a method that updates service fields. After the current requests complete, the data will be saved in the document store.

@Asset

The @Asset annotation marks a service as persistent.

Assets are loaded from the database during initialization, after the @OnInit call and before the @OnActive. Internally, assets use the @OnLoad for a service. Assets are loaded only once, and are in-memory afterwards. Invalidation is not required because assets own their data.

@Modify

@Modify triggers a save for an asset. @Modify methods marks the service as dirty. At the next @AfterBatch, when the service is idle, the asset will save its data.

Using Assets from the Web

Asset services are registered like other objects, using Web.include in the main method. They can be looked up using the requestweb-service call.

See Web for more web-specific documentation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import static io.baratine.web.Web.*;

public class MyMain
{
  public static void main(String []args)
  {
    include(Hello.class);

    get("/hello").to(req->{
      Hello hello = req.service(Hello.class);
      hello.getGreeting(req.of());
    });

    go(args);
  }
}

The previous code is a web application using the Hello asset for its data.

  • For a /hello request, the application looks up the Hello proxy to the Hello asset using requestweb-service.
  • It calls getGreeting using the request web as a continuation for the result.
  • When Hello completes by calling result-ok, the greeting result is chained to the request’s result because of the requestweb-of chain.
  • The request sends the result to its view processing. Because the value is a string, it’s returned as plain text. If the value was an object, it would be returned as JSON.

Vault Service (Map of Assets)

Most assets have many items, such as a bookstore with many books. In Baratine these are Vault services, which includes:

  • A primary key to identify items.
  • Multiple item objects, which share the vault’s service.
  • A Vault acting as a manager for the services.
  • Create methods (optional) to create new items.
  • Find methods (optional) to search the database for items.

Asset CRUD

Create methods in the Vault interface create a new asset. The create generates a new asset identifier, and forwards the create parameters to the new asset. The asset creates and saves itself. Because assets are the sole owner of the data, the vault can’t save the data for the asset.

Reads and updates use methods on the asset, in object-oriented or event-sourcing style. Typically an update method will be a complete operation, such as an auction bid or a book purchase. Fine grained getters and setters are discouraged. The asset should expose its business API, not its implementation details.

Find methods on the Vault can search for an asset by its fields if the id isn’t known. When id’s are known, assets lookup by their service address such as “/book/Xm12N12x”. The lookup API uses Services.service or requestweb-service.

The following RequestWeb example looks up the book proxy using service with the id from the URL. It then calls the book’s get() method to return the book’s contents as a REST value.

1
2
3
4
5
6
@Get("/book/{id}")
public void book(RequestWeb request)
{
  Book asset = request.service(Book.class, request.path("id"));
  asset.get(request.of());
}

Book Vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import io.baratine.service.Result;
import io.baratine.service.Service;
import io.baratine.service.Vault;

@Service
public interface BookVault implements Vault<IdAsset,Book>
{
  void create(Book data, Result<IdAsset> result);

  void findByTitle(String title, Result<Book> result);

  void delete(IdAsset id);
}

Line notes:

    1. @Service is required because vaults need their own service for threading, and load/store lifecycle.
    1. Vaults specify the primary key and the item type.
    1. Optional find methods search the document database. The return is a copy of the book.

Because Vaults are services, they can implement their own methods and have their own data fields, and even persist their data. For simplicity, this example only has the optional finder.

Book Asset

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import io.baratine.service.Result;
import io.baratine.service.Asset
import io.baratine.service.IdAsset
import io.baratine.service.Service;
import io.baratine.service.Modify;

@Asset
public class Book
{
  @Id
  private IdAsset id;
  private String title;
  private String author;

  public void get(Result<Book> result)
  {
    result.okShim(this);
  }

  @Modify
  public void set(Book book, Result<Boolean> result)
  {
    this.title = book.title;
    this.author = book.author;

    result.ok(true);
  }
}

Line notes:

    1. @Asset marks the item as persistent.
    1. IdAsset id the primary key, which is a unique 64-bit long. All Vault item must have a primary key annotated by @Id. Only Baratine may change the id. Application must tread the field as final.
  • 11-12. Fields are loaded and saved automatically from the document database.

    Application code can use fields in-memory without locking, following object-oriented patterns.

    1. A zero-arg constructor is needed so Baratine can create the instance.
    1. Because we’re using Book as its own transfer-object, we need a constructor. A bigger application might create BookData as a dedicated transfer object.
    1. Service methods are typically coarse-grained. A bookstore client will ask for the book data, not its fields individually.
    1. okShim is required for encapsulation because clients never have access to service data.
    1. @Modify marks a method as modifying persistent data. When the method completes, Baratine will save the updated book.
  • 30-31. No locking is needed because the Book is in a service.

Using a Vault from HTTP REST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import io.baratine.service.Result;
import io.baratine.web.*;

public class BookRest
{
  @Get("/book/{id}")
  public void getBook(@Path("id") Book book,
                      Result<Book> result)
  {
    book.get(result.of());
  }

  @Post("/book/{id}")
  public void setBook(@Path("id") Book book,
                      @Body Book newData,
                      Result<String> result)
  {
    book.set(newData,
             result.of(x->"ok"));
  }

  public static void main(String []args)
  {
    Web.include(BookVault.class);
    Web.include(BookRest.class);

    Web.go(args);
  }
}

Line notes:

    1. @Get registers a HTTP GET method with the given path pattern. The {id} will be available as a variable.
    1. @Path converts the {id} into Book proxy by looking up the service with its long key.
    1. The Book result will be rendered as JSON.
    1. book.get calls the book proxy, asynchronously. This method does not block for the get call to complete. Instead the method will return immediately; when the get finishes, it will complete the result. result.of chains the HTTP result into the book get result.
    1. @Post registers a HTTP POST method to update the book.
    1. @Path converts the path into a Book proxy.
    1. @Body deserializes the HTTP body as JSON into a Book transfer object.
    1. The post result is a string. An application would probably return a JSON object.
    1. The book proxy is called with the new data. As before, the call is asynchronous. In fact, we could have returned the HTTP data immediately without waiting for the set to complete, but that might miss any exceptions.
    1. Results are chained using result.of. In this case, we ignore the book.set result and instead return “ok” to HTTP.
    1. Vaults are registered with Web.service like other services.
    1. The HTTP Rest methods are registered with Web.include.
    1. Web.go starts the web server.

Create Methods

Create methods are generally a requirement for vaults. The create is responsible for generating an id and initializing a book. Because of the single-owner principle, create methods have two parts:

  1. The Vault create is responsible for generating the book id.
  2. The Book create is responsible for initializing its data.

Unlike other persistence systems, the Vault can’t save the Book by itself because the Vault doesn’t own the book’s data. Only the Book owns its data.

1
2
3
4
5
@Service
public interface Books implements Vault<IdAsset,Book>
{
  void create(Book book, Result<IdAsset> result);
}

Notes:

  • The application can choose its method arguments. Here we’re using the book class as its own transfer object.
  • The application can also choose its result. Here we’re returning the generated key for the HTTP to encode its URL.
1
2
3
4
5
6
7
8
@Asset
public class Book
{
  @Id
  private IdAsset id;
  private String title;
  private String author;
}

Notes:

  • @Id marks the id field, which should be treated as a final field. The service will fill in the id during creation
  • @Modify is needed for a create to save the initialized book.
  • The book class is used as its own transfer object.
  • Book initializes itself in its create method. No other service can initialize the book’s data.
  • The result of the create is the id, which can be used to generate a book URL.

IdAsset Generation

IdAsset keys are recommended for Vault data. They combine uniqueness with compactness. The keys are generated with the following pattern:

  • 34 bits of time, seconds since the epoch
  • 4-10 bits to identify the node in multi-server configurations
  • 20-26 bits for a per-node sequence

Together the long encodes the creation time of the item, and ensures its uniqueness by avoiding per-node collisions with the time and sequence, and cross-node collisions by encoding the node.

Find Methods

Find methods in a Vault are used for simple queries against the document database. The Result type determines the result.

In the following examples, the result type is determined by the Result. The method names between “find” and “By” are ignored by the parser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import io.baratine.service.Result;
import io.baratine.service.Vault;

public class Books implements Vault<IdAsset,Book>
{
  void findKeyByTitle(String title, Result<Long> result);

  void findBookByTitle(String title, Result<Book> result);
  void findDataByTitle(String title, Result<BookData> result);

  void findProxyByTitle(String title, Result<BookApi> result);
}

Notes:

  • Result<Long> returns the primary key because the key is a long.
  • Result<BookData> returns a new object with fields set to the document fields.
  • Result<Book> does the same as BookData because Book is a class.
  • Result<BookApi> returns a proxy to the book service.

The BookApi proxy can be used to call service methods similar to the example above, where the REST interface used a Book proxy to fulfill the REST call.