A new version of the POLiTe library focuses on the library performance and on the design of new rich-featured cache layer. The cache layer replaces the ObjectBuffer
interface and enhances the concept of indirect memory pointers by adding one more indirection level.
Architecture of the library remained almost unchanged. It is still compiled into one module and used the same way as the original POLiTe library was. Overview of the architecture can be seen in the Figure 3.4, “Architecture of the POLiTe 2 library”
Database layer uses OCI 7 to connect to the Oracle 7 database. The layer has been slightly modified to be able to notify the cache layer of several events like Commit or Rollback.
On the contrary, both the object cache ObjectBuffer
and the object references have been completely replaced by the new cache layer and related infrastructure - database pointers. The differences are explained in the following sections.
Because the new cache layer may use additional maintenance threads, most parts of the library have been made thread-safe. Various synchronisation primitives have been added to the common services. The main architectural change is that all basic database CRUD operations are routed through the cache layer which decides when or how to execute them.
The biggest part of the cache layer represent the provided cache implementations. Cache implementations may derive from one of two interfaces depending on what features they will provide (see the Figure 3.5, “Caches in the POLiTe 2 library”):
Cache
- an interface providing basic synchronous caching functionality.ExtendedCache
- extends theCache
interface with additional methods that are needed for asynchronous maintenance of the cache using the cache managerCacheKeeper
(see later).
Concurrent data access strategies as introduced in the original POLiTe library can be used with caches that implement the ExtendedCache
interface.
Caches can be grouped together using the ComposedCache
class with the help of selectors. Selectors are classes whose instances determine which strategy or which cache should be used based on the given object type, the object itself or based on other custom criteria. The ComposedCache
uses CacheSelector
to decide which cache should be used for the object being processed and then it passes a StrategySelector
to the selected cache as a parameter to each of its operations. Then the cache uses the StrategySelector
to modify its behaviour with selected strategies.
Caches that implement the extended interface can be maintained by the cache manager called CacheKeeper
[13]. CacheKeeper
runs a second thread that scans managed caches and removes instances considered as 'worst'. If dirty, these instances are written back to the database. CacheKeeper
acts also as a facade for composing caches and specifying strategies.
Following cache implementations are provided with the POLiTe 2 library:
VoidCache
- implements the basicCache
interface. This implementation actually does not cache anything, it just holds locked local copies. As the copies are unlocked, changes are propagated immediately to the database and objects are removed from the cache.The
LRUCache
(multithreaded) and theLRUCacheST
(single threaded) implement theExtendedCache
interface. These classes use the LRU[14] replacement strategy. The multithreaded variant can even be used with theCacheKeeper
s' asynchronous maintenance feature. LRU strategy discards least recently accessed items first. It maintains the items in ordered list. New or accessed items are added or moved to the top of the list. Least recently accessed items are pushed towards the bottom from which they are removed. For more information see [09].The
ARCCache
(multithreaded) and theARCCacheST
(single threaded) use the ARC[15] replacement strategy. ARC tries to improve LRU by splitting the cached items into two LRU lists - one for items that were accessed only once (recent cache entries) and second for items that were accessed more than once (frequent cache entries). The cache adapts the size ratio between these two lists. ARC performs in most cases better than LRU, espetially in cases where a larger set of items is accessed (for example a query result is iterated). The cache is not polluted as frequently used items remain in the second list. For more information see [10][11].
Transient object instances are in the original POLiTe library created using standard C++ dynamic allocation - using the new
operator. After making these objects persistent, pointers to these objects are exchanged for indirect references represented by the Ref<T>
template. As the ownership of these objects is transferred to the object cache, the direct pointers may be rendered invalid. In the POLiTe 2 library, the creation and destruction of the objects is managed by the library code and users can access its members by dereferencing indirect pointers.
The indirect pointer template Ref<T>
was replaced by the template DbPtr<T>
. Its instance may be referred to as a database pointer further in the text. DbPtr<T>
is used similarly to the Ref<T>
template. Example 3.5, “Persistent object manipulation in the POLiTe 2 library” demonstrates the creation of a transient instance and its manipulation analogously to the Example 3.3, “Persistent object manipulation”.
Example 3.5. Persistent object manipulation in the POLiTe 2 library
// Create a new employee DbPtr<Employee> e; e->name("Ola Nordmann"); e->age(45); e->salary(60000); e->BePersistent(dbConnection); // Create a new student supervised by the employee created DbPtr<Student> s; s->name("Richard Doe"); s->age(22); s->studcardid("WCD-3223"); s->supervisor(empl); s->BePersistent(dbConnection); // Update the employee's salary e->salary(65000); // There is no Update method as the cache updates the persistent // image automatically according to the Updating strategy in use. // Delete the new student from the database s->DbDelete(); );
The DbPtr<T>
can point to instances in several states:
Transient instances which are present only in memory (and do not have a persistent representation yet), the owner of these instances is the pointer.
In-memory instances owned by a cache (which may or may not have a database representation. This represents in fact two states of the object).
Persistent representation of the object. The pointer contains only the identity of the object.
Last object state, the locked local copy, is implemented as an additional level of indirection when dereferencing the database pointer. By dereferencing it using the *
operator, user receives an instance of a cache pointer (CachePtr<T>
). The existence of the cache pointer guarantees that the object is loaded into the memory and that the memory address of the object will not change until the instance of the cache pointer is destroyed (unlocked). Such process locks the objects in the cache so they cannot be removed unless they are unlocked. The transitions between the object states are displayed in the Figure 3.6, “States of the POLiTe 2 objects”.
By using the *
operator on a database pointer user receives a reference to a loaded and locked instance in a cache. Constructor of the created cache pointer asks cache to load relevant object from the database (if not already present in the cache) and locks the object in the cache. The cache pointer can be dereferenced again resulting in retrieval of direct pointer to the in-memory instance. This process can be simplified just by using the ->
operator on the database pointer. An implicit instance of the cache pointer is created and the ->
operator invoked on it. Then the requested member of the instance is accessed. After that, the cache pointer instance is destroyed and cache lock released. (This may result in significant performance degradation if using the VoidCache, because only locked copies are contained in the cache. Calling the ->
operator causes a persistent object to be loaded from database into the memory, then desired member is accessed and object - if modified - is written back and removed from cache). The process is almost the same as if dereferencing the Ref<T>
references, the difference is the additional level - the cache pointer.
If the variable e
is of the DbPtr<Employee>
type, the following expression:
e->salary(65000);
may trigger a sequence of actions as displayed in the Figure 3.7, “Dereferencing DbPtr in POLiTe 2”
The query language and query classes as described in the section called “Querying” have not been changed in the POLiTe 2 library. Just the query is now executed and its results fetched in a slightly different way:
Example 3.6. Executing queries in the POLiTe 2 library
// All employees with salary > 40000 Query q("Employee::salary > 40000") // Execute q and iterate through the result Result<Employee> result(dbConnection, q); while (++result) { // members of the current object are accessible // using result-> }; result->Close();
New version of the POLiTe library allows users to utilise advanced persistent object caching features. Unmentioned remained the support for multithreaded environment which includes encapsulation of several synchronisation primitives.
Disadvantages listed in the the section called “Conclusion” also apply to this version of the library as they were not the point addressed by the thesis [03].