Sunday, June 26, 2011

STM Arjuna

I'd forgotten how long ago I'd promised to talk about some of the STM work we've been doing. Well I haven't been able to do much more on it for a while, but I do have time to at least outline what's possible at the moment. So let's just remember a bit about the current way in which you can use JBossTS to build transactional POJOs without the need for a database; we'll use an example to illustrate:
  public class AtomicObject extends LockManager
{
public AtomicObject()
{
super();

state = 0;

AtomicAction A = new AtomicAction();

A.begin();

if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
{
if (A.commit() == ActionStatus.COMMITTED)
System.out.println("Created persistent object " + get_uid());
else
System.out.println("Action.commit error.");
}
else
{
A.abort();

System.out.println("setlock error.");
}
}

public void incr (int value) throws Exception
{
AtomicAction A = new AtomicAction();

A.begin();

if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
{
state += value;

if (A.commit() != ActionStatus.COMMITTED)
throw new Exception("Action commit error.");
else
return;
}

A.abort();

throw new Exception("Write lock error.");
}

public void set (int value) throws Exception
{
AtomicAction A = new AtomicAction();

A.begin();

if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
{
state = value;

if (A.commit() != ActionStatus.COMMITTED)
throw new Exception("Action commit error.");
else
return;
}

A.abort();

throw new Exception("Write lock error.");
}

public int get () throws Exception
{
AtomicAction A = new AtomicAction();
int value = -1;

A.begin();

if (setlock(new Lock(LockMode.READ), 0) == LockResult.GRANTED)
{
value = state;

if (A.commit() == ActionStatus.COMMITTED)
return value;
else
throw new Exception("Action commit error.");
}

A.abort();

throw new Exception("Read lock error.");
}

public boolean save_state (OutputObjectState os, int ot)
{
boolean result = super.save_state(os, ot);

if (!result)
return false;

try
{
os.packInt(state);
}
catch (IOException e)
{
result = false;
}

return result;
}

public boolean restore_state (InputObjectState os, int ot)
{
boolean result = super.restore_state(os, ot);

if (!result)
return false;

try
{
state = os.unpackInt();
}
catch (IOException e)
{
result = false;
}

return result;
}

public String type ()
{
return "/StateManager/LockManager/AtomicObject";
}

private int state;
}
Yes, quite a bit of code for a transactional integer. But if you consider what's going on here and why, such as setting locks and creating potentially nested transactions within each method, it starts to make sense. So how would we use this class? We'll let's just take a look at a unit test to see:
     AtomicObject obj = new AtomicObject();
AtomicAction a = new AtomicAction();

a.begin();

obj.set(1234);

a.commit();

assertEquals(obj.get(), 1234);

a = new AtomicAction();

a.begin();

obj.incr(1);

a.abort();

assertEquals(obj.get(), 1234);
But we can do a lot better in terms of ease of use, especially if you consider what's behind STM: where objects are volatile (don't survive machine crashes). And this is where our approach comes in. Let's look at the example above and create an interface for the AtomicObject:
public interface Atomic
{
public void incr (int value) throws Exception;

public void set (int value) throws Exception;

public int get () throws Exception;
}
And now we can create an implementation of this:
@Transactional
public class ExampleSTM implements Atomic
{
@ReadLock
public int get () throws Exception
{
return state;
}

@WriteLock
public void set (int value) throws Exception
{
state = value;
}

@WriteLock
public void incr (int value) throws Exception
{
state += value;
}

@TransactionalState
private int state;
}
Here we now simply use annotations to specify what we want. The @Transactional is needed to indicate that this will be a transactional object. Then we use @ReadLock or @WriteLock to indicate the types of locks that we need on a per method basis. (If you don't define these then the default is to assume @WriteLock). And we use @TransactionalState to indicate to the STM implementation which state we want to operate on and have the isolation and atomicity properties (remember, with STM there's no requirement for the D in ACID). If there's more state in the implementation than we want to recover (e.g., it can be recomputed on each method invocation) then we don't have to annotate it.

But that's it: the AtomicObject and ExampleSTM classes are identical. So let's take a look at another unit test:
      RecoverableContainer theContainer = new RecoverableContainer();
ExampleSTM basic = new ExampleSTM();
Atomic obj = obj = theContainer.enlist(basic);
AtomicAction a = new AtomicAction();

a.begin();

obj.set(1234);

a.commit();

assertEquals(obj.get(), 1234);

a = new AtomicAction();

a.begin();

obj.incr(1);

a.abort();

assertEquals(obj.get(), 1234);
Think of the RecoverableContainer as the unit of software transactional memory, within which we can place objects that it will manage. In this case ExampleSTM. Once placed, we get back a reference to the instance within the unit of memory, and as long as we use this reference from that point forward we will have the A, C and I properties that we want.

1 comment:

Joseph Ottinger said...

Where do @ReadLock and @WriteLock come from?