The implementation of the IDataCacheProvider provided by com.agfa.pacs.memcache is based on the original Tiani JVision cache design with various improvements.

Features

The DataCache class implements the functionality defined by The IDataCacheProvider with the option to de-activate disk cache and use only the java heap for persistent data (Persistence between application starts is of course not possible without disk). To eliminate the possibility of data corruption due to wrong usage of the cache or cache implementation bugs, DataCache tracks data put into the cache and returned to the application to detect multiple returns of the same object to the cache (wrong cache method usage, application bug) or multiple use of the same object by the cache (which would indicate a bug within the cache).

General Structure

The main IDataCacheProvider interface is implemented by DataCache. Cached instances are managed by instances of ICacheItemGroup. The individual groups contain management structures for pooling, personalied and persistent item management. The actual grouping of items into groups depends on the type of cached object. It makes sense group items which can be passed between the pooling and caching functionality. For this reason, arrays of the same primitive type and size are managed by a single ICacheItemGroup instance. Individual cached object instances are ancapsulated into CacheItem instances which keep track of their state (reuseable, referenced, ...). A background thread, implemented by the PoolGuard class is responsible for lazy write and cleanup of cached data. UML com.agfa.pacs.cache.jpg should be here, didn't get it working :-(

Memory Mangement Integration

The cache itself is registered as a MemoryAlertListener. Whenever an allocation attempt fails, the cache is the first listener notified to free up resources. Only if this does not gain sufficient free heap space, other listeners are called. Items are deleted on a LRU basis across the cache (with lowest priority items being deleted first), using the GlobalLRU list of DataCache. In addition clean-ups caused by the alerts raised by the cache itself, the cache receives callbacks on MemoryNotificationInfo.MEMORY-COLLECTION-THRESHOLD-EXCEEDED fired by the MemoryMXBean, which cause the cache to intensify data disk persistence. The size of the cache relative to the overall heap size (Defined in the CacheParameters class).

Additionally a parameter keepFootprintSmall can force the cache to generally prefer re-use over allocation of new items. This policy is only recommended if the impact of the cache on the overall heap allocation has to be minimized, for example when running in a VM instance with a very small heap, or running as a part of an application which is to a large extent not aware of the existence of the cache and it's heap house-keeping mechanisms.

Disk Persistence

The following assumes that the cache is operated with UseDisk set to true.

Items added to the cache using one of the addPersistentXXX() methods are stored into the configured cache directory on disk. The type of the Item's CacheID and the actual persistence implementation of the ICacheItemGroup which is responsible for managing the item determine the actual location of the data on disk. Anonymous CacheIDs (created with createID()) and CacheIDs created with a persistence type PERSISTENCE_TEMPORARY are stored into a subdirectory named "@hostname@timestamp@temp" which is deleted at application shutdown (or next start if the shutdown was a crash). CacheIDs with PERSISTENCE_PERMANENT are stored into subdirectory "perm". CacheIDs created with PERSISTENCE_ETHERNAL are stored in subdirectory "ethe". PERMANENT and TEMPORARY items are subject to disk quota. If the cache size exceeds a specified limit, items may be removed (see below for details). ETHERNAL items are never removed automatically and should be used with care.

If a CacheID consists of a group and an item (the usual case for non-temporary items) it will be stored in a sub-directory named according to the group ID. Data belonging to a study could for example use the StudyInstanceUID as the group ID. The default storage layout for primitive type arrays is to use a distinct file for each array. Other types of data may use a coommon file for all items of a cache group.

At startup time, the content of the cache is scanned by a background thread and registered in the DiskItemsHash member of DataCache and marked as untouched. TEMPORARY data possibly left over from a previous run is deleted, also PERSISTENT items may be deleted if the cache size exceeds quota. Deletion is performed at group level on a least recently used basis using the time of last read access to the data. If disk size thresholds is exceeded during the execution of the software, data on disk has to be deleted. First items not accessed during the current application run will be deleted (LRU). If an item is read from disk it is marked as touched. Items which are not used any longer by the application are also get marked as untouched and will be deleted the next time disk space runs low. When new persistent items are added to the cache or existing ones are marked as modified by the releaseModifiedReference() method, the corresponding item is added to the persistenceQueue member of DataCache. The content of the queue is consumed in the background by the PoolGuard thread, writing persistent data to disk. The persistenceQueue is a priority queue, with items of the same priority processed on an LRU basis. The consumption of the queue affects low-priority items first, resulting in low priority items being written first as they are more likely to be re-used to deliver different content soon. Synchroneous writes may happen as a result of invokung flush() or if object allocation forces a re-use of the persistent object.

For file I/O of primitive arrays one of two mechanisms is used: the native library nativeio is preferrred if available on the specific platform delivers best performance. As a fallback, java.nio classes are used. Custom object handlers may use any I/O mechanism.

Configuration Options

The following parameters of the cache are configured through agility's configuration interface:

Re-use and Priority

All internal data structures used to manage and re-use cache items are priority queues with LRU handling of items with the same priority. Each item has a priority associaed with it, determining the likeliness of it being re-claimed and used for a different purpose soon. When a new object is requested from the cache (by allocXXX() or internally when allocating a target for reading data from disk), the pool functionality of the cache is employed to avoid allocation of new objects by new[]. The behavior of CachedObjectGroup.allocateObject() - the method used by all item group implementations of the cache proceeds as follows:

CacheItem Lifecycle

A cache item can have several different states (ItemState enum). The most important states are:

Several additional states are used to represent transient states associated with writing and reading persistent data (see Implemntation details section)

Extensibility

The memcache provides an extension point "com.agfa.pacs.memcache.CachedObjectHandler" which allows to register handlers for arbitrary classes. This allows to provide any combination of the pool/cache/persistence functionalities for arbitrary user-defined objects. An implementation of ICachedObjectHandler has to be provided which is able to create ICachedObjectGroup instances responsible for managing the desired type of object. When implementing a handler for a new object type, it is strongly recommended to subclass either CommonFileGroup, DistinctFileGroup or BlockedFileGroup instead of directly implementing the ICachedObjectGroup interface, as this significantly reduces the complexity of implementation. as they provide an interface of significantly reduced complexity. The sub-classes may choose to implement or omit the pool and/or persistent cache functionalities. Personalized data handling is mandatory, as it is required for the implementation of persistent data.