Introduction
Goals and Rationale
Package Structure
Caching API
Locking API
Using the Caching and Locking Services
Notes on Service Implementations
The concurrency packages, org.jasig.portal.concurrency.*, are offered as building blocks for portal developers who want to create persistence support for portal entities. The locking and caching packages are self-contained “mini-frameworks” exposed as portal services via classes in org.jasig.portal.services.
The concurrency framework operates on entities, persistent objects uniquely identified by a type and a key. As currently implemented, these packages cache entities in memory for fast access and lock entities for either reading or writing. Perhaps in the future, we will abstract the operations of creating, finding and updating them.
Goals and RationaleThe need for these services arose because of performance problems in the groups framework. Groups are relatively expensive to create and are frequently reused. They needed to be cached, and since they are updatable, they also needed to be locked. Caching and locking objects in a single JVM is fairly simple, but it quickly gets complicated when the caching and locking must work across multiple JVMs and servers.
There are some decent commercial caching products available, but they are not appropriate for an open source project. Unfortunately, the open source solutions I looked at were too big, too ugly, too early or just not right (I know, picky, picky.) I think the most promising is Turbine’s Java Caching System or JCS (see http://jakarta.apache.org/turbine/jcs), an approximate implementation of the JCache specification (see http://jcp.org/jsr/detail/107.jsp), which in turn, describes the Oracle9i caching framework. JCS promises to be an efficient, full-featured framework, but it is still in development and it does not do locking. So, at least in the interim, uPortal needs some home grown locking and caching support. The concurrency packages are intended as a common solution that can be used not just by groups but by any type of portal entity and replaced when a better alternative is found. They are not intended to replace or compete with full-featured commercial or open source frameworks.
Package Structure The concurrency services operate on IBasicEntities, objects
that implement org.jasig.portal.IBasicEntity
, defined as
follows:
public interface IBasicEntity {
public org.jasig.portal.EntityIdentifier getEntityIdentifier();
}
An EntityIdentifier has a key and type that uniquely identify its underlying entity. Two EntityIdentifiers with the same key and type refer to the same underlying entity (although they may belong to different IBasicEntities.)
The package org.jasig.portal.concurrency
defines
some basic types to support concurrency: a lock (IEntityLock), a cache
(IEntityCache), and services for locking and caching (IEntityLockService
and IEntityCachingService). It also contains the locking and
caching Exceptions:
org.jasig.portal.concurrency
IEntityCache
IEntityCachingService
IEntityCachingServiceFactory
IEntityLock
IEntityLockService
IEntityLockServiceFactory
CachingException
LockingException
The individual locking and caching packages contain reference implementations of the basic types, plus some supporting classes.
Caching APIorg.jasig.portal.concurrency.caching
CachedEntityInvalidation
LRUCache
RDBMCachedEntityInvalidationStore
ReferenceEntityCache
ReferenceEntityCachingService
ReferenceEntityCachingServiceFactory
ReferenceInvalidatingEntityCache
org.jasig.portal.concurrency.locking
EntityLockImpl
IEntityLockStore
MemoryEntityLockStore
RDBMEntityLockStore
ReferenceEntityLockService
ReferenceEntityLockServiceFactory
IEntityCachingService defines a minimal api for caching and
retrieving IBasicEntities. An IEntityCachingService manages a
number of IEntityCaches. Each IEntityCache holds IBasicEntities of
a given type, (the type returned by IEntityCache.getEntityType()
).
This type must be known to the portal (see org.jasig.portal.EntityTypes
.)
In a multi-server environment, caches must be able to synchronize
themselves with their peers on other servers, but this is part of their
implementation and not part of the api:
Locking APIpublic interface IEntityCachingService {
void add(IBasicEntity entity) throws CachingException;
IBasicEntity get(Class type, String key);
void remove(Class type, String key);
void update(IBasicEntity entity) throws CachingException;
}
The interface IEntityLockService defines an api for acquiring
lock objects, IEntityLocks, that can be used to control concurrent
access to IBasicEntities. The sub-type of IBasicEntity must be
known to the portal (see org.jasig.portal.EntityTypes
.)
A lock is associated with a particular entity and has an owner, a
lockType and an expirationTime. Currently supported lock types are
IEntityLockService.READ_LOCK
, which is shared, and IEntityLockService.WRITE_LOCK
,
which is exclusive. To lock an entity for update, ask the
service for a write lock:
int lockType = IEntityLockService.WRITE_LOCK;
IBasicEntity entity = getEntityFromSomewhere();
IEntityLock lock = svc.newLock(entity, lockType, lockOwner);
If there is no conflicting lock on the entity, the service responds with the requested lock. If another lock owner holds a conflicting lock, the service throws a LockingException. If the service returns the lock, no other client will get be able to get a conflicting lock. From then on, communication with the service is via the lock:
lock.convert(int newType);
lock.isValid();
lock.release();
lock.renew();
The complete service api is as follows:
Using the Caching and Locking Servicespublic interface IEntityLockingService {
public void convert(IEntityLock lock, int newType)
throws LockingException;
public void convert(IEntityLock lock, int newType, int durationSecs)
throws LockingException;
public boolean existsInStore(IEntityLock lock)
throws LockingException;
public boolean isValid(IEntityLock lock)
throws LockingException;
public IEntityLock newLock
(IBasicEntity entity, int lockType, String owner)
throws LockingException;
public IEntityLock newLock
(IBasicEntity entity, int lockType, String owner, int
durationSecs)throws LockingException;
public void release(IEntityLock lock)
throws LockingException;
public void renew(IEntityLock lock)
throws LockingException;
public void renew(IEntityLock lock, int durationSecs)
throws LockingException;
}
The caching and locking APIs are accessed by portal clients
via service façades located in org.jasig.portal.services
,
EntityCachingService and EntityLockService. These classes serve
as bootstraps for service implementations specified in
portal.properties. One way to plug in an external locking or
caching service would be to create an alternate implementation of the
service that adapts the external service to the portal interface.
The caching façade simply duplicates the api of the service interface. Caching consists of asking the service to add, retrieve, update and remove elements from the cache, e.g.:
// Retrieve the entity from its store:
String key = getEntityKey();
IBasicEntity ent = findEntity(key);
// Cache the entity:
EntityCachingService.add(ent);
...
// Retrieve the entity from the cache:
Class type = getEntityClass();
IBasicEntity anotherReferenceToTheEntity = EntityCachingService.get(type, key);
...
// Change the entity and then notify the cache:
EntityCachingService.update(ent); // notifies peer caches.
// Or delete the entity and notify the cache:
EntityCachingService.remove(type, key); // notifies peer caches.
The EntityLockService façade lets clients do one single thing, acquire a lock on an entity:
Or perhaps:IBasicEntity entity = getEntityFromSomewhere(type, key);
String owner = getThePortalUserId();
IEntityLock lock =
EntityLockService.instance().newWriteLock(entity, owner);
IEntityLock lock = EntityLockService.instance().
newWriteLock(entity, owner, durationSecs);
Once the client has the lock, it communicates with the underlying service implementation via the lock:
Notes on Service Implementationslock.convert(IEntityLockService.WRITE_LOCK); // convert read to write
...
if ( lock.isValid() ) // check if lock has expired
{
entity.update();
lock.release();
}
Common properties.
The portal.properties file has a section labelled "Concurrency
Services settings" with values for configurable concurrency
properties. There are 2 properties common to concurrency
services, multi-server, which
indicates if the portal will run in multiple JVMs, and clockTolerance, which can be set to
account for differences among system clocks on different hosts:
# Concurrency Services settings:
#
# multiServer (true/false) indicates if the portal will run in
multiple jvms.
#
# clockTolerance (in milliseconds) sets a fudge factor to account
for system clocks
# on different hosts. Only used when
org.jasig.portal.concurrency.multiServer=true.
#
# Defaults: multiServer=false
#
clockTolerance=5000
#
org.jasig.portal.concurrency.multiServer=false
org.jasig.portal.concurrency.clockTolerance=5000
These properties apply to both concurrency services. The full
list of properties is:
Service |
Property |
Description |
concurrency |
multiServer |
Indicates if the
portal will run in multiple JVMs. Default=false. |
concurrency |
clockTolerance |
Fudge factor in
milliseconds for system clocks. Only used when
multiServer=true. Default=5000. |
Locking | IEntityLockServiceFactory |
Service
factory. Default=ReferenceEntityLockServiceFactory. |
Locking |
defaultLockDuration |
Default value in
seconds, may be overridden at request time. Default=300. |
Caching |
IEntityCachingServiceFactory |
Service
factory. Default=ReferenceEntityCachingServiceFactory |
Caching |
defaultMaxCacheSize |
Default for maximum
number of cache entries. Can be overriden for a specific cache
type. Default=1000. |
Caching |
maxCacheSize |
(Optional) Maximum
number of cache entries for a specific cache. See
portal.properties for examples. |
Caching |
defaultSweepInterval | Default in seconds
for interval between sweeps. Can be overridden for a specific
cache type. Default=60. |
Caching |
sweepInterval |
(Optional) Interval
in seconds between sweeps for a specific cache. See
portal.properties for examples. |
Caching |
defaultMaxIdleTime |
Default in seconds
for the maximum time a cache entry can remain untouched before being
reaped. Can be overridden for a specific cache type.
Default=1800. |
Caching |
maxIdleTime |
(Optional) Maximum
time in seconds a cache entry can remain untouched before being reaped,
for a specific cache. See portal.properties for examples. |
Single-server vs. multi-server. In single-server mode, locks are held in memory. There is no need to invalidate entries in peer caches because there are no peer caches. In multi-server mode, locks are stored in the portal database, which is shared by all portal instances. Each time a cache entry is updated or removed, the cache adds an invalidation to the invalidation store, which is likewise held in the portal database. Caches periodically retrieve invalidations from the store and purge invalid entries.
Multi-server mode and pooled database connections.
If the portal is running on a single server, concurrency services
should run in single-server mode, since running in multi-server mode
imposes extra database activity. However, if the portal is running
in a multi-server environment, you must run in multi-server mode for
locking and caching to function properly. In multi-server mode,
these services make frequent hits to the portal database using
connections obtained from org.jasig.portal.RDBMServices
.
As a result, RDBMServices must have a source of pooled database
connections, or else the concurrency services themselves will become
performance bottlenecks.
The Cache Cleanup Cycle. Each cache instance runs a cleanup thread that periodically wakes up and invokes a cleanup routine to purge the cache of stale entries. The interval between cleanups is configurable in portal.properties. In multi-server mode, the cache first retrieves and processes its invalidations. In either case, if the cache is larger than its maximum size, the cleanup routine removes least recently used cache entries until the cache no longer exceeds its maximum size.
Future enhancements. Caches are now created as needed. Perhaps they should be created and pre-populated on service start-up, kicked off by an entry in services.xml. The caching properties might be better represented in xml format.