
$Id: UseCases.txt,v 1.1 2003/11/18 03:48:40 bwang00 Exp $



                          Entity bean cache
                          =================




A. All nodes share the same database
------------------------------------

Each node has a cache in front of the database

When an entity bean is to be loaded, and is not in the cache, the
cache loader will load it from the database into the cache

Each EJB will have pointcuts (or isModified() ?) defined which emit a
notification on state change, we will therefore know when a bean has
been changed

Until a bean has been changed, we never go to the database, but only
read values from the cache. Once a bean has been changed, we go to the
database and open a transaction. At the same time we'll broadcast a
cache invalidation message for that bean, which causes all nodes to
remove the bean from their cache. Next time they access the bean it
will be read from the database. All updates are then written to the
database when the transaction commits.

Possibly, writes are only performed by 1 member (the coordinator). Otherwise,
everyone would write the same data to DB.

So, in summary, we cache read-only, and handle writes by using the
underlying database's transactions. The diff to the current commit
option B/C is that read-access is cached with the new solution.


B. All nodes have their own local database
------------------------------------------

Same as above, but we need to lock all caches before writes.

Updates are propagated to all nodes by locking the nodes first,
replicating the changes and finally committing or aborting the
changes. Each cache will then use a transaction to apply the change to
the local database


EntityBeanCache on a single node (non-cluster setup)
---------------------------------------------------------------

The EntityBeanCache is configured as an MBean (transient, no replication)
 
The EJBInterceptor receives an invocation on an EntityBean

The EntityBean is not in the cache. The EntityBean interceptor fetches multiple
rows from the DB and creates an EJB. The EJB is added to the cache, key is primary key and
value is the EJB

From now on, the EJB is in the cache. Reads will be served from the cache. Writes will
lock the EJB, perform the updates and on TX commit write the changes to the DB.


EntityBeanCache on a cluster
----------------------------------

The EntityBeanCache is configured as an MBean (replicated, with locking)

The EJBInterceptor receives an invocation on an EntityBean

The EJB is not in the cache

The EJBInterceptor reads multiple rows from the DB and provides the EJB.

The EJB is put in the cache. This causes 1 replication message to be sent to all
nodes. Each node does the following:
- The entire cache is locked (cache-lock) and the EJB is added
- A lock is acquired on the EJB only (primary key, row-lock)
- The entire cache is unlocked (cache-lock)

If any node cannot acquire the cache-lock and row-lock, an exception is
thrown and we retry. At the end of this, all nodes should have a (row-)lock
on the EJB. Locking the entire cache should only be minimal, and is only
used on insertion and removal, not on updates to individual EJBs (which
is probably the most frequent access pattern).

The EJBInterceptor receives more updates to the same EJB. Those are
only applied to the cache, not the DB. Each cache update is stored locally, and
is not yet replicated.

The transaction commits.

The cache replicates the changes to the EJB to all nodes (1 replication message).

Each node applies the temporary changes to the cache (the EJB-lock is held). Here, we
can use Bill's AOP stuff to determine which attributes have changed. This would allow us
to
(a) determine read-access. If no fields were changed we don't replicate
(b) determine the modified attributes. Then we only replicate the changed attributes. This
is only useful for EJBs with a large state though.

Each node commits the changes to the DB. I guess this is done by the EJBInterceptor.

Each node releases the row-lock on the EJB. Other transactions can now access the EJB.


Additions to the above
---------------------------

The same algorithm can be basically applied to Stateful Session Beans (minus
the DB part)



Read and Write access to an EntityBean
------------------------------------------------

In the EntityBeanInterceptor: when the current thread is associated with a transaction,
we have a write access. Otherwise we have a read access to the EntityBean. (Deployment
descriptors define which methods require a transaction (write-access) and which not
(read-access). Only after write-access do we write the bean back to the database. Read-access
never goes to the DB.

A read access locks the bean only locally (with a read-lock see below)),
and unlocks it after the method has completed. This is to ensure that the local value
is not being modified (e.g. by a write-access) while accessing the bean.

A write access acquires the lock for the bean in *all* cache instances (via a multicast,
pessimistic locking)).
(This might throw a LockingException if some other node has locked that specific bean,
and hasn't released it within a certain timeout).

When the transaction is committed, another multicast message is sent to all cache instances,
containing the new state of the bean. That state is updated everywhere and the lock is released.
This will always succeed because every cache instance already holds the lock for the bean.

This requires 2 multicasts for a write-transaction.

We can later introduce optimistic locking where we only lock the bean locally on a
write-access. At the end of the transaction, we multicast the new state to all cache instances.
If  the bean was modified by a different node in the meantime, this multicast will terminate
with exceptions, and we need to rollback the update (using another multicast).

Therefore optimistic locking typically uses only 1 multicast for a write-transaction, and 2
if there was a locking conflict.

Lock concurrency: read-locks are concurrent; when a bean is locked by a read-lock,
another read-lock will succeed.

When a bean is locked by a read-lock, another write-lock has to wait until the read-lock
is released.

When a bean is locked by a write-lock, another read-lock has to wait until there are no
more write-locks (even pending write-locks get prefertial treatment over read-lock requests).

When a bean is locked by a write-lock, another write-loock has to wait until the lock is
released.

For these scenarios we can use the JGroups RWLock (borrowed from Jini), or we can
use some of the r/w locks provided by concurrent.util.



Interaction with EJB Interceptor
--------------------------------

EJB interceptor is a user of the Cache and will also be called by the
Cache. Needs to implement interface

   readObject(in objectkey) --> CacheValue [/** read multiple as an option as well ? **/]
   writeObject(in list of <objectkey,CacheValue> pairs)
   prepare(in list of <objectkey,CacheValue> pairs) [optional]
   commit() [optional]
   rollback() [optional]

Whenever there is a new TX, the EJB interceptor calls Cache.begin(Xid).

Reads are done calling Cache.get(). Value might be a hashmap, consisting of attribute names as
keys and attribute values as values.

If a Cache entry is not present, the Cache will call readObject() on the registered EJB
interceptor (cache loader), accessing the database.

Writes are done using put().

Note, that so far, the database has not yet been accessed (except in the case when an item was
not in the cache).

When the TX commits, Cache.commit() should be called.

At this point we have 2 options: asynchronously replicate the modifications, or synchronously
replicate the modifications.


(1) Asynchronous replication:

The EJB interceptor's writeObject() method writes all changes to the database (starting a
transaction, writing the changes, committing the transaction).

After the transaction has committed (writeObject() returns successfully, causing Cache.commit()
to return successsfully), the changes are added to the replication queue as an item. The items
in the queue will periodically be replicated to the backup node(s).

(2) Synchronous replication:

All changes are replicated to all Cache nodes in the cluster (PREPARE phase) in one multicast
message. We then wait for all responses.

On reception, every node (also the sending one) acquires local locks on the Cache and stores
all changes in limbo ((transient) storage area different from cache). When all locks could be
acquired successfully, a member returns OK, otherwise FAIL. (FAIL might for example be returned
if the item on which a lock needs to be acquired has been modified in the meantime (version is
different).

Once all responses have been received, if all responses are OK, we multicast a COMMIT
message. Otherwise we multicast a ROLLBACK message.

[optional] Before returning an OK, the Cache might at this point call the EJB interceptor's
prepare(0 method to make sure the database also acquires locks (not just the cache).

On reception of a ROLLBACK message, each member releases the locks held by that transaction and
removes the data in limbo for that transaction.

On reception of a COMMIT message, each member adds the data in limbo for that transaction to
the cache and then calls the EJB interceptor's writeObject() method with the changed items in
the cache as arguments. (Alternatively, if prepare() was previously called on the EJB
interceptor with the changed items, we just call commit() on the EJB interceptor). All locks in
the cache are now released.


So, in summary, interactions are as follows:

(a) EJB interceptor calls Cache.begin(), Cache.commit() or Cache.rollback() (XAResource interface)
(b) Cache calls EJB interceptors readObject(), writeObject() and (optionally) prepare()/commit()/rollback().










