Skip to content
Jan Wiemer edited this page Apr 11, 2018 · 20 revisions

Java ACI Store - a transient and transactional store for Java objects.

The name JACIS of the store is derived from the acronym ACID (*A*tomicity, *C*onsistency, *I*solation, *D*urability) describing the properties of transactions. The store is designed to fulfill the first three of these properties but not the Durability.

Quick Start

The easiest way to use JACIS is to use e.g. gradle or another build tool that can manage maven repositories. The URL of a maven repository containing all JACIS releases is: https://github.com/JanWiemer/jacisreleases/raw/master/maven-repository/

A sample gradle build file may look like:

apply plugin: 'java'
apply plugin: 'maven'
sourceCompatibility = 1.8
version = '1.0'
group = 'org.jacisclientexample'
jar {
    manifest {
        attributes 'Implementation-Title': 'Jacis Client Example',
                   'Implementation-Version': 1.0,
                   'Implementation-Vendor' : "Jan Wiemer"
     }
 }
repositories {
    mavenCentral()
     maven {
        url 'https://github.com/JanWiemer/jacisreleases/raw/master/maven-repository/'
    }
}
dependencies {
    compile group: 'org.jacis', name: 'jacis', version: '1.28'
}

A little example project using JACIS con be found here: https://github.com/JanWiemer/jacisexampleprj

First we create a simple example class of objects that shall be stored in a transactional store. The class implements the JacisCloneable interface to enable the store to clone the object without using Furthermore the class extends the AbstractReadOnlyModeSupportingObject (implementing the JacisReadonlyModeSupport interface). This means the object provides a secure read only mode. It is the responsibility of an object implementing the JacisReadonlyModeSupport interface to prevent any modification after the method switchToReadOnlyMode is called. The AbstractReadOnlyModeSupportingObject provides a checkWritable method throwing an exception if the object is in read only mode.

  public static class Account extends AbstractReadOnlyModeSupportingObject implements JacisCloneable<Account> {

    private final String name;
    private long balance;

    public Account(String name) {
      this.name = name;
    }

    @Override
    public Account clone() {
      try {
        Account clone = (Account) super.clone();
        return clone;
      } catch (CloneNotSupportedException e) { // can never happen
        throw new InternalError("Failed to clone " + this + "! " + e);
      }
    }

    Account deposit(long amount) {
      checkWritable();
      balance += amount;
      return this;
    }

    public Account withdraw(long amount) {
      checkWritable();
      balance -= amount;
      return this;
    }

    public String getName() {
      return name;
    }

    public long getBalance() {
      return balance;
    }

  }

First we initialize a JACIS container:

    JacisContainer container = new JacisContainer();

Now we create a store for our example object:

    JacisObjectTypeSpec<String, Account> objectTypeSpec = new JacisObjectTypeSpec<>(String.class, Account.class);
    JacisStore<String, Account> store = container.createStore(objectTypeSpec);

Now we start a local transaction on the JACIS container (that means on all stores of this container):

    JacisLocalTransaction tx = container.beginLocalTransaction();

Now we can work with the store (inside a transaction) and create and update objects. First we create a new Account:

    Account account1 = new Account("account1");

All modifications of objects have to be notified to the store explicitly (there is no automatic dirty checking). This is true for modifications as well as for creating new objects. In both cases the store is notified by calling the update method:

    store.update(account1.getName(), account1);

Enough for our first transaction. Now we commit the transaction. Afterwards all other transactions can see our new Account

    tx.commit();

For our next transaction we use a helper method executing the passed code (as lambda expression) inside a transaction. We use this to deposit some money on our account. Note that the call of the update is again necessary. If we omit this call the change will be lost after commit (try it).

    container.withLocalTx(() -> {
      Account acc = store.get("account1");
      acc.deposit(100);
      store.update("account1", acc);
    });

We use another transaction to check the balance of the Account:

    container.withLocalTx(() -> {
      Account acc = store.get("account1");
      System.out.println("balance of " + acc.getName() + ": " + acc.getBalance());
    });

Finally we withdraw some money and simulate an exception causing the transaction to be rolled back:

    try {
      container.withLocalTx(() -> {
        Account acc = store.get("account1");
        acc.withdraw(10);
        store.update("account1", acc);
        throw new RuntimeException("Error in transaction!");
      });
    } catch (RuntimeException e) {
      System.out.println("Expected exception " + e);
      // expected
    }

Again we check the balance of the Account to see that the transaction failed and nothing is withdrawn:

    container.withLocalTx(() -> {
      Account acc = store.get("account1");
      System.out.println("balance of " + acc.getName() + ": " + acc.getBalance());
    });

Accessing a JACIS Store using the Stream API

The API of the JACIS store provides access to the stored objects using a Java 8 type stream API. First we create some example objects to have some data to play around:

    container.withLocalTx(() -> {
      store.update("account0", new Account("account0").withdraw(100));
      store.update("account1", new Account("account1").deposit(100));
      store.update("account2", new Account("account2").deposit(200));
      store.update("account3", new Account("account3").deposit(300));
      store.update("account4", new Account("account4").deposit(400));
      store.update("account5", new Account("account5").deposit(500));
      store.update("account6", new Account("account6").deposit(600));
      store.update("account7", new Account("account7").deposit(700));
      store.update("account8", new Account("account8").deposit(800));
      store.update("account9", new Account("account9").deposit(900));
    });

Now we show some examples how to use the stream API:

    // To cumulate values usually read only access is enough (this is possible without a transaction)
    System.out.println("sum=" + store.streamReadOnly().mapToLong(acc -> acc.getBalance()).sum());

    // streaming the objects starting with a filter
    System.out.println("#>500=" + store.streamReadOnly(acc -> acc.getBalance() > 500).count());

    // as an example to modify some objects add 10% interest to each account with a positive balance
    container.withLocalTx(() -> {
      store.stream(acc -> acc.getBalance() > 0).forEach(acc -> {
        store.update(acc.getName(), acc.deposit(acc.getBalance() / 10));
      });
    });

    // finally output all accounts
    String str = store.streamReadOnly().//
        sorted(Comparator.comparing(acc -> acc.getName())). //
        map(acc -> acc.getName() + ":" + acc.getBalance()).//
        collect(Collectors.joining(", "));
    System.out.println("Accounts: " + str);
  }

Tracked views

Tracked views can be used to track values derived from the original stored object automatically up to date. This avoids the need to compute the values again and again. When creating the store the tracked views (implementing the TrackedView<ObjectType> interface where ObjectType is the class of the stored objects) are registered at the tracked view registry of the store. Later it is possible to access a snapshot of the tracked view by calling the getView method at the tracked view registry. Note that the snapshot includes all changes done inside the transaction where it was taken and all committed changes by other transactions.

First we show an example of a tracked view tracking the total balance of all stored accounts:

  public static class TotalBalanceView implements TrackedView<Account> {

    private long totalBalance = 0;

    // tracked views must be cloneable to create the snapshots...
    @SuppressWarnings("unchecked")
    @Override
    public TrackedView<Account> clone() {
      try {
        return (TrackedView<Account>) super.clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException("clone failed");
      }
    }

    // the method actually tracking the modification of an object
    // (for creations / deletions the oldValue / newValue is null)
    @Override
    public void trackModification(Account oldValue, Account newValue) {
      totalBalance += newValue == null ? 0 : newValue.getBalance();
      totalBalance -= oldValue == null ? 0 : oldValue.getBalance();
    }

    // An (optional) method to check the value of the view.
    // This is called after commit if configured in the JacisObjectTypeSpec passed to create the store.
    @Override
    public void checkView(List<Account> values) {
      long checkValue = values.stream().mapToLong(a -> a.getBalance()).sum();
      if (totalBalance != checkValue) {
        throw new IllegalStateException("Corrupt view! Tracked value=" + totalBalance + " computed value=" + checkValue);
      }
    }

    @Override
    public void clear() {
      totalBalance = 0;
    }

    public long getTotalBalance() {
      return totalBalance;
    }

  }

The tracked view is registered after creation of the store. Later it is possible to get a snapshot of the view either inside or without a transaction.

    JacisContainer container = new JacisContainer();
    JacisObjectTypeSpec<String, Account> objectTypeSpec = new JacisObjectTypeSpec<>(String.class, Account.class);
    JacisStore<String, Account> store = container.createStore(objectTypeSpec);

    // First register the tracked view

    store.getTrackedViewRegistry().registerTrackedView(new TotalBalanceView());

    // First we create some accounts to have some test data...

    container.withLocalTx(() -> {
      store.update("account1", new Account("account1").deposit(-100));
      store.update("account2", new Account("account2").deposit(10));
      store.update("account3", new Account("account3").deposit(100));
    });

    // on commit the tracked view is updated automatically
    TotalBalanceView view0 = store.getTrackedViewRegistry().getView(TotalBalanceView.class);
    System.out.println("tracked balance=" + view0.getTotalBalance());

    // inside a transaction the transaction local view of the values is respected
    container.withLocalTx(() -> {
      store.update("account1", store.get("account1").deposit(1000));
      store.update("account4", new Account("account4").deposit(101));
      // note that the getView method takes a snapshot at the time it is called...
        TotalBalanceView view1 = store.getTrackedViewRegistry().getView(TotalBalanceView.class);
        System.out.println("tracked balance=" + view1.getTotalBalance());
        // later updates are not tracked by this snapshot
        store.update("account1", store.get("account1").deposit(1000));
        System.out.println("tracked balance old snapshot=" + view1.getTotalBalance());
        TotalBalanceView view2 = store.getTrackedViewRegistry().getView(TotalBalanceView.class);
        System.out.println("tracked balance new snapshot=" + view2.getTotalBalance());
      });