Chapter 11. Accessing Coherence from C++

Coherence C++ is the latest addition to the Coherence product family. Shipped as a pure native library, Coherence C++ is made available for a select but growing number of platforms. As of Coherence 3.5, support is available for recent versions of Windows, Linux, Solaris, and Apple OS X—in both 32 and 64 bit variants.

Because Coherence C++ is a Coherence*Extend client, the available feature set is virtually identical to what is offered with the Coherence Java and .NET Extend clients. This includes the basic get/put cache access, as well as advanced features such as near cache, continuous query cache, events, entry processors, aggregators, and queries.

The C++ client used the Coherence Java client as a guide in terms of class and method names and signatures. The result is that it is quite easy to transfer your newly acquired Coherence Java knowledge to C++.

Configuring Coherence C++

Coherence C++ supports the same configuration mechanisms, which you've seen being used in the Java and .NET editions. The C++ variants are modeled as a subset of the Java edition, and are DTD-based XML files. Because of this, it is unnecessary to reiterate most of the configuration options; you can reuse the cache and operational configuration files that had been built up for Java Extend clients.

There are exceptions worth noting. For instance, the system property configuration override feature of the Java client is available to C++ clients in a somewhat different form—in that environment variables are used. In some Unix shells, '.' delimited environment variables may not be supported. For this reason, you may also specify the property names in camel case, so tangosol.coherence.override and TangosolCoherenceOverride are considered to be equivalent. Another notable configuration difference in comparison with Java is that for advanced configurations where custom classes are supplied via configuration, you would make use of C++ naming conventions rather the Java ones, that is, '::' rather than '.' class name delimiters.

POF configuration is where C++ differs from Java and .NET. Currently, C++ POF configuration is programmatic rather then declarative. This means that you will need to include your POF type ID to class registration within your code. This is accomplished via a simple one-line addition to your data object's .cpp file. POF configuration will be covered in detail later when we demonstrate how to serialize your data objects.

If included in the application's startup directory, the cache and optional operational configuration files will be automatically loaded without any additional effort. The default filenames are inherited from the Java client, namely coherence-cache-config.xml and tangosol-coherence-override.xml. If an alternative location or filename is used, you will need to tell Coherence C++ where to look for these files. This can be done programmatically by calling the CacheFactory::configure method, which takes up to two XmlElement configuration descriptors as input. The first argument is the cache configuration and the optional second argument is the operational configuration. By taking in XmlElements rather than file descriptors or names, it allows the application to determine how the configuration content is obtained. As the most likely means of doing so is from a file, convenience helpers are also included so that you can easily load the XML configuration from a file as follows:

CacheFactory::configure(
CacheFactory::loadXmlFile("c:/config/MyCacheConfig.xml"));

Alternatively, you can use environment variables to override the default name and location if you wish to avoid specifying them programmatically. The names are again inherited from Java. The tangosol.coherence.cacheconfig property specifies the path to the cache configuration, and tangosol.coherence.override specifies the path for the operational configuration.

Managed object model

The Coherence C++ API is built on top of a managed object model, which takes on the responsibility for object lifecycle management. This means you will not make explicit calls to new or delete when dealing with managed objects. A managed object is an instance of any class that derives from coherence::lang::Object. This base class is responsible for maintaining the object's reference count, allowing for automatic destruction once the object is no longer being referenced.

This model is useful for Coherence because caches both return and retain references to their cached objects. A cache may be a subset of a larger data set and may evict an item while the application could still be using a previously obtained reference to the same item. The result is that there is no clear owner for cached items, and thus no answer to the question of who should take responsibility for deleting these objects. The managed object model removes this issue by taking over the ownership, ensuring that objects are automatically deleted once they are no longer being referenced.

Application code interacts with these managed objects not via traditional C++ raw pointers, or references, but via smart pointers that transparently update the managed object's reference count, but are otherwise used much the same way as raw pointers. While many C++ smart pointer implementations will declare the smart pointer for a given class Foo via templates (that is smart_ptr<Foo> and smart_ptr<const Foo>), the Coherence managed object model uses nested typedefs to declare the smart pointers for a class (that is Foo::Handle and Foo::View).

Handles, Views, and Holders

For each managed class, there are three smart pointer typedefs: Handle, View, and Holder.

For a given managed class Foo:

  • Foo::Handle is the functional equivalent of Foo*

  • Foo::View is the functional equivalent of const Foo*

  • Foo::Holder is the functional equivalent of a union of a Foo* and const Foo*

A Holder acts just like a View, but allows for a safe cast attempt to a Handle. Unlike the standard C++ const_cast<> the Coherence cast<> is safe and only succeeds if the Holder had been assigned from Handle. A Holder is typically used with containers such as coherence::util::Collection, which allows the caller to choose to store either a Handle or View within the collection.

The assignment rules for the smart pointers follow the same rules as regular pointers:

  • A View may be assigned from a Handle, View, or Holder.

  • A Holder may be assigned from a Handle, View, or Holder, and will remember the smart pointer type it has been assigned from.

  • A Handle may be assigned from a Handle, or from a Holder via cast<>.

  • A smart pointer for a parent class may be assigned from the smart pointer of a derived class.

  • A smart pointer may only be assigned from a non-derived class via cast<>.

Managed object creation

To properly maintain the reference count, it is important that the objects are only referenced via smart pointers, and never via raw pointers. As such, creation is not performed via the traditional new operator, but instead by a static create factory method (for example, Foo::create()). This factory method returns the initial Handle, ensuring that raw pointers are avoided from the point of object creation.

The factory methods are auto-generated via inherited template definitions, and take parameter sets matching the class's constructors. The constructors are declared as protected, to ensure that these objects are not allocated by other means.

Casting and type checking

As previously mentioned, the object model also includes a helper function for performing dynamic casts. Its functionality is roughly equivalent to the standard C++ dynamic_cast, although it is specialized to work with managed classes and their smart pointers. For example, given an Object::View, we can attempt to cast it to a String::View as follows:

Object::View v = ...
String::View vs = cast<String::View>(v);

If the View referenced something other than a String, then a ClassCastException would be thrown from the cast<> function call. There is also a related instanceof<> helper function, which will identify if a cast will succeed.

Object::View v = ...
if (instanceof<String::View>(v))
{
// v references a String
}

These functions work similarly for a Handle, View, or a Holder. In the case of a Holder, they are also the mechanism for extracting a stored Handle. The following cast will succeed if the Holder was assigned from String::Handle:

Object::Holder oh = ...
String::Handle hs = cast<String::Handle>(oh);

Handling exceptions

Error conditions in Coherence C++ are communicated by throwing exceptions. The managed object model defines a hierarchy of managed exceptions, rooted at coherence::lang::Exception. A special COH_THROW macro is used to throw and re-throw managed exceptions. This macro will, if possible, record a full stack trace into the exception to aid error diagnosis.

The exceptions are caught via their nested (or inherited) View type:

try
{
...
COH_THROW (IOException::create("test"));
...
}
catch (IOException::View vexIO)
{
// handle IOException
}
catch (Exception::View vex)
{
// print exceptions description and stack trace
std::cerr << vex << std::endl;
COH_THROW (vex); // re-throw
}

Managed exceptions may also be caught as std::exceptions, allowing pre-existing error handling logic to handle them as well. For instance, a thrown IOException could also be caught as follows:

try
{
...
COH_THROW (IOException::create("test"));
...
}
catch (const std::ios_base::failure& exIO)
{
// handle IOException or other
}
catch (const std::exception& ex)
{
// handle any Exception or std::exception
std::cerr << ex.what() << std::endl;
throw;
}

Class hierarchy namespaces

The Coherence classes are organized into a set of namespaces based on their functionality. Their header files are organized similarly. For example, coherence::lang::Object is defined in coherence/lang/Object.hpp.

For convenience, there is a special header file for each namespace, which includes all headers for that namespace. This mechanism along with a using namespace statement is often used to bring the entire coherence::lang namespace into your application code, while explicit class headers and using statements are preferred for classes outside of the coherence::lang namespace:

#include "coherence/lang.ns"
#include "coherence/net/CacheFactory.hpp"
#include "coherence/net/NamedCache.hpp"
using namespace coherence::lang;
using coherence::net::CacheFactory;
using coherence::net::NamedCache;

For the sake of simplicity, the examples in the remainder of the chapter do not show the include or using statements.

This basic introduction of the managed object model is enough to get us started with Coherence C++ coding. The object model, internally known as Sanka, also contains a large number of generic utility classes, which reside mostly in the coherence::lang and coherence::util namespaces. You'll find that these classes and the model itself have a noticeable Java inspiration to them. The Coherence C++ cache classes are also inspired by their Java counterparts. The result is that those familiar with Coherence Java should find Coherence C++ quite easy to learn.

There are many more details to the managed object model, but those details are beyond scope of this book. The Coherence C++ product documentation contains an in-depth guide to the object model, and it is recommended that you familiarize yourself with the object model in greater depth as you begin the work of building your Coherence C++ based solutions.

Implementing a Coherence C++ client application

In this section, we will finally write some C++ cache-related code. Let's start by obtaining a NamedCache from the CacheFactory:

NamedCache::Handle hCache = CacheFactory::getCache("accounts");

We can then proceed to operate on the cache in much the same way as we did in Java:

String::View vsKeySRB = "SRB";
String::View vsKeyUSA = "USA";
hCache->put(vsKeySRB, String::create("Serbia"));
hCache->put(vsKeyUSA, String::create("United States"));
hCache->put(vsKeyUSA, String::create("United States of America"));
hCache->remove(vsKeySRB);
std::cout << vsKeyUSA << " = " << hCache->get(vsKeyUSA) << std::endl;
std::cout << "Cache size = " << hCache->size() << std::endl;
hCache->clear();

The C++ NamedCache interface contains the full method set from the Java NamedCache, allowing access to other standard operations such as putAll and getAll.

You'll notice that, unlike Java, there is no automatic conversion support from quoted string literals to Coherence managed objects, except for methods whose signature states that they take a String, such as CacheFactory::getCache(String::View). Thus to create and pass a String to a method such as NamedCache::put(Object::View, Object:Holder), we must explicitly call String::create() to produce our managed String. String objects can also be assigned from and to std::string and std::wstring.

Implementing Cacheable C++ Data Objects

The previous example demonstrated how to use the basic cache APIs to store strings using C++. Of course, we want to work with more interesting types than just strings. Let's define a simple C++ class that will represent a pre-existing custom data object used in your application, which you'd like to cache.

class Account
{
// ----- data members -------------------------------------------
private:
const long long m_lId;
std::string m_sDescription;
Money m_balance;
long long m_lLastTransactionId;
const long long m_lCustomerId;
// ----- constructors --------------------------------------------
public:
Account(long long lId, const std::string& sDesc,
const Money& balance, long long lLastTransId,
long long lCustomerId)
: m_lId(lId), m_sDescription(sDesc), m_balance(balance),
m_lLastTransactionId(lLastTransId),
m_lCustomerId(lCustomerId)
{
}
Account()
: m_lId(0), m_lLastTransactionId(0), m_lCustomerId(0)
{
}
// ----- accessors -----------------------------------------------
public:
long long getId() const
{
return m_lId;
}
std::string getDescription() const
{
return m_sDescription;
}
void setDescription(const std::string& sDesc)
{
m_sDescription = sDesc;
}
Money getBalance() const
{
return m_balance;
}
long long getLastTransactionId() const
{
return m_lLastTransactionId;
}
long long getCustomerId() const
{
return m_lCustomerId;
}
};

Can we just pass an instance of this into the NamedCache::put() method? Unfortunately, it is not quite that simple.

Remember that the caching API deals with managed objects as there is no clear owner for a piece of cached data. Our plain old Account class is definitely not managed. Aside from it being a managed class, there are a few other basic requirements for cached data as well:

  • It should implement Object::hashCode/equals (for keys)

  • It should implement Object::clone (for values)

  • It should be POF serializable

In many cases, it may not be desirable to retrofit your data objects to be managed, as this could impose some far-reaching application-level changes. For this reason, the Coherence API includes a Managed<> template adapter, which will adapt pre-existing classes so they may be stored in Coherence.

Managed adapter

In order to be compatible with the Managed template, the data object class must have:

  • A public or protected zero parameter constructor

  • A copy constructor

  • An equality comparison operator

  • A std::ostream output function

  • A hash function

The Managed adapter will implement the initial set of requirements, delegating where applicable to the previously described functions. Our Account class already meets the first two requirements. So all that is needed to make it Managed-compatible is to define three functions as follows:

bool operator==(const Account& accountA, const Account& accountB)
{
return accountA.getId() == accountB.getId() &&
accountA.getDescription() == accountB.getDescription() &&
accountA.getBalance() == accountB.getBalance() &&
accountA.getLastTransactionId() ==
accountB.getLastTransactionId() &&
accountA.getCustomerId() == accountB.getCustomerId();
}
std::ostream& operator<<(std::ostream& out, const Account& account)
{
out << "Account("
<< "Id=" << account.getId()
<< ", Description=" << account.getDescription()
<< ", Balance=" << account.getBalance()
<< ", LastTransactionId=" << account.getLastTransactionId()
<< ", CustomerId=" << account.getCustomerId()
<< ')';
return out;
}
size_t hash_value(const Account& account)
{
return (size_t) account.getId();
}

As you can see, adding these functions is quite simple and does not require that the data object takes on any awareness of Coherence. Now it becomes possible to use Managed<Account> in our code:

// construct plain old Account object
Account account(32105, "checking", Money(7374, 10, "USD"), 55, 62409);
// construct managed key and value
Integer64::Handle hlKey = Integer64::create(32105);
Managed<Account>::Handle hAccount = Managed<Account>::create(account);
// cache hAccount
hCache->put(hlKey, hAccount);
// retrieve the cached value
Managed<Account>::View vResult = cast<Managed<Account>::View>( hCache->get(hlKey));
std::cout << "retrieved " << vResult << " from cache for key "
<< vResult->getId() << std::endl;
// convert the cached value back to a non-managed type
Account accountResult(*vResult);

This code demonstrates how a Managed<Account> instance is created from a non-managed Account data object.

The create method delegates to the copy constructor on the Account class&mdash;thus, the Managed<Account> instance is a copy of and retains no references to the Account instance it was constructed from. Managed<Account> is then inserted into the cache using its identifier as a key. Next, it is extracted from the cache. The NamedCache::get() operation returns an Object::Holder, which then must be dynamically cast back to the expected data object class. Finally, the Managed<Account> instance is used to construct a non-managed Account, which can be used by pre-existing application logic.

Note that it is certainly allowed for the application to use the Managed<Account> object directly, because all of Account's public methods are still accessible as is demonstrated when we call the getId method.

Data object serialization

The requirement to address is serialization, which when using the Managed adapter is accomplished by implementing two additional free functions:

template<> void serialize<Account>(PofWriter::Handle hOut,
const Account& account)
{
hOut->writeInt64(0, account.getId());
hOut->writeString(1, account.getDescription());
hOut->writeObject(2, Managed<Money>::create( account.getBalance()));
hOut->writeInt64(3, account.getLastTransactionId());
hOut->writeInt64(4, account.getCustomerId());
}
template<> Account deserialize<Account>(PofReader::Handle hIn)
{
long long lId = hIn->readInt64(0);
std::string sDesc = hIn->readString(1);
Managed<Money>::View vBalance = cast<Managed<Money>::View>(
hIn->readObject(2));
long long lTransId = hIn->readInt64(3);
long long lCustomerId = hIn->readInt64(4);
return Account(lId, sDesc, *vBalance, lTransId, lCustomerId);
}

Notice that Account includes a data member Money, which is an instance of another plain old C++ class. To serialize this nested object, we simply write it out as a Managed<Money> object, applying the same patterns as were used for Managed<Account>. The serialization functions obviously have Coherence awareness, and thus it may not be desirable to declare them inside the same source file as that of the Account class. The sole requirement is that they are defined within some .cpp file and ultimately linked into your application. Interestingly, they do not need to appear in any header file, which means that they could be put into something like a standalone AccountSerializer.cpp file, without the need to modify the Account.hpp/cpp that is used in application code.

We must also register our serializable Managed<Account> class with the Coherence C++ library. This is accomplished via a simple call to a macro.

COH_REGISTER_MANAGED_CLASS(POF_TYPE_ACCOUNT, Account);

The registration statement specifies the POF type ID to class mapping. Unlike the Java and .NET versions, in C++, this mapping is performed at compilation time. The registration statement relies on the declaration of the serialization functions, and is therefore typically part of the same source file. Note the registration macro does not need to be called as part of the application logic&mdash;it is triggered automatically as part of static initialization.

The only thing missing now is the definition of POF_TYPE_ACCOUNT, which could just be a #define statement to the numeric POF type ID for the Account class. While you may just choose to embed the ID number directly in the registration line, it is recommended that #define, or some other external constant be used instead. Defining an external constant allows for the creation of a PofConfig.hpp file for the application that includes all the POF type IDs. This results in a single place to perform the ID assignment, so you do not have to search through the various data-object serialization files to adjust any of the IDs. Here is an example of this PofConfig.hpp file:

#ifndef POF_CONFIG_HPP
#define POF_CONFIG_HPP
#define POF_TYPE_ACCOUNT 1000
#define POF_TYPE_MONEY 1003
#define POF_TYPE_TRANSACTION 1004
#define POF_TYPE_TRANSACTION_ID 1005
#define POF_TYPE_TRANSACTION_TYPE 1006
#define POF_TYPE_DEPOSIT_PROCESSOR 1051
#define POF_TYPE_WITHDRAW_PROCESSOR 1050
#define POF_TYPE_CURRENCY 2000
#endif // POF_CONFIG_HPP

This header file is analogous to the pof-config.xml file we would have used in Java or .NET. With these last pieces in place, our example will now work with both local and remote caches, automatically being serialized as needed.

Implementing managed classes

It is, of course, possible to directly implement a managed class as well, in which case we can also implement the PortableObject interface or, make use of PofSerializer. While it may be unlikely that you would choose to implement your cached data types directly as managed classes, it is the normal pattern for custom implementations of entry processors, map listeners, and other Coherence-related classes.

To demonstrate the process, let's rewrite our Account sample class as a managed class. In doing so, we will also choose to make use of the Coherence included types for its data members.

class Account
: public cloneable_spec<Account>
{
friend class factory<Account>;
// ----- data members --------------------------------------------
private:
const int64_t m_lId;
MemberView<String> m_sDescription;
MemberHandle<Money> m_balance;
int64_t m_lLastTransactionId;
const int64_t m_lCustomerId;
// ----- constructors --------------------------------------------
protected:
Account(int64_t lId, String::View sDesc, Money::View balance,
int64_t lLastTransId, int64_t lCustomerId)
: m_lId(lId), m_sDescription(self(), sDesc),
m_balance(self(), balance),
m_lLastTransactionId(lLastTransId),
m_lCustomerId(lCustomerId)
{
}
Account()
: m_lId(0), m_sDescription(self(), sDesc),
m_balance(self(), balance), m_lLastTransactionId(0),
m_lCustomerId(0)
{
}
Account(const Account& that)
: m_lId(lId), m_sDescription(self(), sDesc),
m_balance(self(), cast<Money::View>(balance->clone()),
m_lLastTransactionId(lLastTransId),
m_lCustomerId(lCustomerId)
{
}
// ----- accessors -----------------------------------------------
public:
virtual int64_t getId() const
{
return m_lId;
}
virtual String::View getDescription() const
{
return m_sDescription;
}
virtual void setDescription(String::View sDesc)
{
m_sDescription = sDesc;
}
virtual Money::View getBalance() const
{
return m_balance;
}
virtual int64_t getLastTransactionId() const
{
return m_lLastTransactionId;
}
virtual int64_t getCustomerId() const
{
return m_lCustomerId;
}
// ----- Object methods ------------------------------------------
virtual bool equals(Object::View vThat) const
{
if (!instanceof<Account::View>(vThat))
{
return false;
}
Account::View that = cast<Account::View>(vThat);
return this == that || (
getId() == that->getId() &&
getLastTransactionId() == that->getLastTransactionId() &&
getCustomerId() == that->getCustomerId() &&
getDescription()->equals(that->getDescription()) &&
getBalance()->equals(that->getBalance));
}
//optional ostream output function
virtual void toStream(std::ostream& out) const
{
out << "Account("
<< "Id=" << getId()
<< ", Description=" << getDescription()
<< ", Balance=" << getBalance()
<< ", LastTransactionId=" << getLastTransactionId()
<< ", CustomerId=" << getCustomerId()
<< ')';
}
virtual size32_t hashCode() const
{
return (size32_t) getId();
}
};

Overall, the code isn't much different from the original. Let's go through the differences one by one.

Understanding specifications

Perhaps, the strangest looking bit is the inheritance statement:

public cloneable_spec<Account>

This is part of the object model, and is called a "spec"-based class definition, where spec is short for specification. Specs do a fair amount of boiler-plate code injection to make the authoring of new managed classes easier than it would otherwise be. For instance, the following items (and more) are injected:

  • Implied virtual inheritance from Object, making it managed

  • Defined Account::Handle/View/Holder nested smart pointer typedefs

  • Defined Account::super typedef to parent class

  • Added static create methods to match Account's constructors, returning Account::Handle

  • Added clone() method implementation that delegates to Account's copy constructor

There is an entire family of specs. In the previous example, we used cloneable_spec because the item is going to be stored within a cache and needs to be cloneable. The other types of specs are:

  • class_spec&mdash;the most basic of specs, which just defines a managed class

  • cloneable_spec&mdash;a class_spec that supports cloning

  • abstract_spec&mdash;defines a non-instantiable class with a partial implementation

  • interface_spec&mdash;defines a non-instantiable class with all pure virtual methods

  • throwable_spec&mdash;defines a spec-based exception class

All specs will automatically add inheritance from Object. Each will also add in some specific features of its own. Specs take the following template parameters:

spec<class, extends<parent>, implements<interface, ...> >

The arguments for the spec are:

  • class: Required, specifies the name of the class being defined

  • extends<parent>: Optional, specifies a class to derive from, defaults to extends <Object>, not included for interface_spec

  • implements<interface1, interface2, ...>: Optional, the list of interfaces that this class implements, defaults to implements<>

Factory methods

The next related part is:

friend class factory<Account>;

This friend declaration allows the auto-generated create methods to access the protected constructors. It is the only bit of boiler-plate that specs cannot inject themselves.

Member variables

Next, we can look at the data member declarations:

private:
const int64_t m_lId;
MemberView<String> m_sDescription;
MemberHandle<Money> m_balance;
int64_t m_lLastTransactionId;
const int64_t m_lCustomerId;

First we switch our long long data members to use int64_t. On most modern C++ compilers, long long is a 64-bit integer type, but it is not required to be of a specific size. The int64_t is a fixed-sized type, which is guaranteed to be 64 bits wide. It is part of a family of fixed-size types that exist on many systems&mdash;for those on which it does not, Coherence adds the definitions. By convention, managed types use fixed-size primitives, though this is not a requirement.

Next, we switch from std::string to a Coherence managed String, referenced by a View. The Money class is similarly overhauled, allowing us to reference it via Handle. You are not required to change these types&mdash;it is done here to improve serialization efficiency by avoiding the need to perform type conversion during serialization later on.

You'll also notice that, for String and Money, we used MemberView<String> and MemberHandle<Money> rather than nested String::View, and Money::Handle. This is done because the nested Handle/View/Holder smart pointer types are not thread-safe. As objects stored in a cache could be accessed by multiple threads, it is important that they internally be thread-safe. MemberHandle/View/Holder are thread-safe variants and should be used as data member references.

This doesn't mean you should avoid using the nested smart pointer types. In fact, they are what you will use most often. They are used as local variables and function/method parameters, basically anything that is stack allocated. Note that using the nested types for data members would have compiled and appeared to function just fine, but there would have been a memory leak/corruption looming. It is, therefore, highly recommended that you use the thread-safe variants for data members of all managed classes.

There are two additional thread-safe smart pointer variants included with Coherence C++:

  • FinalHandle/View/Holder&mdash;an immutable smart-pointer data member

  • WeakHandle/View/Holder&mdash;a weak reference-style smart pointer3

The Final variants are similar in functionality to const MemberHandle/View/Holder, but include object model performance benefits based on the awareness that it is immutable. The Weak variants are used in conditions where your object graph includes cycles. As the object model makes use of reference counting, a cyclical graph would result in a memory leak. These "weak" smart pointers avoid the leak by automatically being NULL'ed out once they are the sole reference to an object, thus allowing the object to be collected. The use of these variants is otherwise identical to the Member variants.

Implementing constructors

Moving right along, we get to the constructors:

protected:
Account(int64_t lId, String::View sDesc, Money::View balance,
int64_t lLastTransId, int64_t lCustomerId)
: m_lId(lId), m_sDescription(self(), sDesc),
m_balance(self(), balance),
m_lLastTransactionId(lLastTransId),
m_lCustomerId(lCustomerId)
{
}
Account()
: m_lId(0), m_sDescription(self(), sDesc),
m_balance(self(), balance), m_lLastTransactionId(0),
m_lCustomerId(0)
{
}
Account(const Account& that)
: m_lId(lId), m_sDescription(self(), sDesc),
m_balance(self(), cast<Money::View>(balance->clone()),
m_lLastTransactionId(lLastTransId),
m_lCustomerId(lCustomerId)
{
}

First we notice that the constructors are now declared as protected, which blocks both stack-based as well as operator new-based allocations, leaving the static create method as the only allocation mechanism.

Next we see that MemberView and MemberHandle are initialized with self(). The thread-safe smart pointers take an optional second parameter, which is the object they are to reference, and if left out, defaults to NULL. The self() used in initialization is an easy pattern to follow, though perhaps a bit difficult to understand.

Each managed object contains an embedded micro read/write lock, which is used to provide the thread-safety to its smart pointer data members. The smart pointers thus require a reference to their enclosing object, and this is exactly what self() returns. Specifically, the self() method returns a reference to the base class of the managed object being created. It is conceptually similar to the this pointer, except that it references the fully initialized base class, while this refers to the partially initialized derived type.

Note that the thread-safe smart pointers can be used outside of managed classes as well. In this case, you will need to provide them a surrogate object that will protect them, as there is no self. This surrogate can either be an object you allocate yourself, or you can use one from a pool obtained by calling System::common().

Implementing methods

The next change is that the methods are now all declared as virtual. This is certainly not required, but in general, managed classes are designed to operate like Java where all methods are virtual.

Finally, we override some standard methods declared by Object. These allow for hashing, equality testing, and printing of the object.

Implementing the PortableObject interface

This was a bit of a detour, but finally, we can get back to making our class implement PortableObject. To do this, we'll simply modify the inheritance statement to indicate that this class implements PortableObject, and then implement its methods and register the type:

class Account
: public cloneable_spec<Account,
extends<Object>,
implements<PortableObject> >
{
...
// ----- PortableObject methods --------------------------------
public:
virtual void writeExternal(PofWriter::Handle hWriter) const
{
hWriter->writeInt64(0, getId());
hWriter->writeString(1, getDescription());
hWriter->writeObject(2, getBalance());
hWriter->writeInt64(3, getLastTransactionId());
hWriter->writeInt64(4, getCustomerId());
}
virtual void readExternal(PofReader::Handle hReader)
{
m_lId = hReader->readInt64(0, getId());
setDescription(hReader->readString(1));
setBalance(cast<Money::Handle>(hReader->readObject(2)));
setLastTransactionId(hReader->readInt64(3));
m_lCustomerId = hReader->readInt64(4);
}
};
COH_REGISTER_PORTABLE_CLASS( POF_TYPE_ACCOUNT, Account); // must be in .cpp

So after a lot of explanation, the act of making the class POF serializable is quite trivial. You will notice that in readExternal, we need to set two const data members, which is not allowable. This issue exists because PortableObject deserialization occurs after the object has already been instantiated. To achieve const-correctness, we would unfortunately need to either remove the const modifier from the declaration of these data members, or cast it away within readExternal.

Implementing external serializer

The final serialization option available to us is to write an external serializer for our managed data object. Here we'll create one for the non-PortableObject version of the managed Account class. Note that the serializer-based solution does not exhibit the const data member issues we encountered with PortableObject.

class AccountSerializer
: public class_spec<AccountSerializer,
extends<Object>,
implements<PofSerializer> >
{
friend class factory<AccountSerializer>;
public:
virtual void serialize(PofWriter::Handle hWriter, Object::View v)
const
{
Account::View vAccount = cast<Account::View>(v);
hWriter->writeInt64(0, vAccount->getId());
hWriter->writeString(1, vAccount->getDescription());
hWriter->writeObject(2, vAccount->getBalance());
hWriter->writeInt64(3, vAccount->getLastTransactionId());
hWriter->writeInt64(4, vAccount->getCustomerId());
hWriter->writeRemainder(NULL); // mark end of object
}
virtual Object::Holder deserialize(PofReader::Handle hReader)
const
{
int64_t lId = hReader->readInt64(0);
String::View sDesc = hReader->readString(1);
Money::Handle hBalance = cast<Money::Handle>(
hReader->readObject(2));
int64_t lTransId = hReader->readInt64(3);
int64_t lCustomerId = hReader->readInt64(4);
hReader->readRemainder(); // read to end of object
return Account::create(lId, sDesc, hBalance, lTransId,
lCustomerId);
}
};
COH_REGISTER_POF_SERIALIZER(POF_TYPE_ACCOUNT,
TypedClass<Account>::create(),
AccountSerializer::create()); // must be in .cpp

All in all, this is pretty much the same as we'd done in Java, the prime difference being the COH_REGISTER statement that registers AccountSerializer and Account class with the Coherence library. The usage of the new managed Account class is somewhat more direct than with Managed<Account>:

// construct managed key and value
Account::Handle hAccount = Account::create(32105, "checking",
Money::create(7374, 10, "USD"), 55, 62409);
Integer64::Handle hlKey = Integer64::create(32105);
// cache hAccount
hCache->put(hlKey, hAccount);
// retrieve the cached value
Account::View vResult = cast<Account::View>(hCache->get(hlKey));
std::cout << "retrieved " << vResult << " from cache for key "
<< vResult->getId() << std::endl;

In later sections, we'll make use of specs to write other types of custom classes such as filters and aggregators.

Executing queries

Coherence C++ offers a QueryMap interface that closely resembles its Java counterpart:

class QueryMap
: public interface_spec<QueryMap,
implements<Map> >
{
public:
virtual Set::View keySet(Filter::View vFilter) const;
virtual Set::View entrySet(Filter::View vFilter) const;
virtual Set::View entrySet(Filter::View vFilter,
Comparator::View vComparator) const;
virtual void addIndex(ValueExtractor::View vExtractor,
bool fOrdered, Comparator::View vComparator);
virtual void removeIndex(ValueExtractor::View vExtractor);
};

Executing a query is a simple matter of constructing the filter to identify the record matching criteria, and then supplying it to either the keySet or entrySet methods. The Comparator variants can be used to order the result set if necessary.

Unless operating on a local cache, the supplied filters, extractors, and comparators are only used as serializable stubs, as all processing will be done remotely in Java on the cache servers.

Value extractors

Central to QueryMap is the concept of the ValueExtractor, which is used to extract an embedded value from a cached entry. For instance, you can use a value extractor to extract the balance from our Account data object.

These extractors are used both in expressing the filter criteria and in applying indexes to the cache in order to optimize query performance. Included with Coherence C++ is a ReflectionExtractor implementation, which is suitable for queries against remote caches, so long as the cache server contains a Java version of the data object:

hCache->addIndex(ReflectionExtractor::create("getBalance"), false, NULL);

Extracting values from locally cached objects

As the C++ language does not have reflection support, it should come as no surprise that the ReflectionExtractor throws an UnsupportedOperationExpection if it is used against a C++ local cache. If you intended to perform queries against local caches, it would appear that you would be left with writing your own custom C++ extractors for each of your data objects to obtain embedded values. While this is not terribly difficult, it is also thankfully unnecessary. Coherence C++ includes a TypedExtractor that utilizes a combination of macros, templates, and function pointers to do a fairly decent job of emulating the Java ReflectionExtractor.

ValueExtractor::View vExtractor = COH_TYPED_EXTRACTOR(Money::View, Account, getBalance);

The macro parameters are the type of the extracted value, the class type to perform the extraction on, and finally, the const accessor method used to obtain the value. In the case of accessor methods that return non-managed types, there is a BoxExtractor variant, which will wrap the primitive type back into its corresponding managed type, which is required to implement the ValueExtractor interface.

ValueExtractor::View vExtractor =
COH_BOX_EXTRACTOR(Integer64::View, Account, getId);

Note that if you are working with the Managed<> template helper, you need to use a special COH_BOX_MANAGED_EXTRACTOR version instead.

To make things even nicer, these extractors actually extend ReflectionExtractor, and thus can be used against both local and remote caches, so long as the cache server has the corresponding Java version of the class and uses the same method names.

PofExtractor

The final type of built-in extractor included with Coherence C++ is the PofExtractor. As described earlier, PofExtractor allows for efficient extraction on the server side without the need for full de-serialization, or the corresponding Java classes.

ValueExtractor::View vExtractor = PofExtractor::create(typeid(Money), Account::BALANCE);

When using PofExtractor, it is best practice to create static identifiers for the property indexes. The previous example assumes that we've defined one for the BALANCE field.

In Coherence 3.5, C++ PofExtractor can only be applied to remote caches. This is for two reasons&mdash;for one, local caches hold onto data in the object rather the serialized form, and secondly (largely because of the first reason), the C++ PofExtractor is currently implemented as a stub, because its primary use is to be evaluated on cache servers within the cluster.

Implementing PropertyExtractor in C++

In Java and .NET, we've introduced PropertyExtractor, and you might find it useful to have the same in C++. This can be accomplished easily enough with a new macro helper around TypedExtractor.

#define COH_PROPERTY_EXTRACTOR(TYPE, CLASS, PROPERTY) 
coherence::util::extractor::TypedExtractor< 
TYPE, CLASS, &CLASS::get##PROPERTY> 
::create(COH_TO_STRING("get" << #PROPERTY));

This allows usage like the following:

ValueExtractor::View vExtractor =
COH_PROPERTY_EXTRACTOR(Money::View, Account, Balance);

We will need an additional version if we wish to handle Managed<> data objects, such as our original Managed<Account>:

#define COH_MANAGED_PROPERTY_EXTRACTOR(TYPE, CLASS, PROPERTY) 
coherence::util::extractor::BoxExtractor< 
TYPE, CLASS, &CLASS::get##PROPERTY, 
coherence::lang::Managed<CLASS>::Holder> 
::create(COH_TO_STRING("get" << #PROPERTY));

The usage remains quite similar:

ValueExtractor::View vExtractor =
COH_MANAGED_PROPERTY_EXTRACTOR(Money::View, Account, Balance);

Ok, these aren't quite the same as our other PropertyExtractor implementations, but they are close enough to be just as useful. The key differences are that property names are capitalized, it assumes a get accessor, and that on the Java side it will deserialize and execute as plain old ReflectionExtractor rather than a PropertyExtractor.

It is certainly possible to create a full fledged C++ PropertyExtractor implementation; it would closely follow the pattern laid out in TypedExtractor whose source is entirely within the TypedExtractor.hpp header.

Filters

Coherence C++ ships with the same built-in filter set as that provided for Java. These filters are capable of executing against both local and remote caches. When chaining together the built-in filters is not sufficient to express your query, you are free to also write custom filters. Unless the filters will only be targeted at a local cache, you will also need to produce a Java version, as that is what will actually do the filtering when running against a remote cache.

While the logic within Java and C++ implementations doesn't have to be identical, it is important that the implementations are compatible, which implies:

  • For a given set of inputs, both implementations should produce the same result

  • The serialized form of both implementations should be equivalent

Performing a query in C++

Now we can put all these things together and finally perform a query from within C++:

// add an index for the description property on the Account class
ValueExtractor::View vExtractor =
COH_PROPERTY_EXTRACTOR(String::View, Account, Description);
hCache->addIndex(vExtractor, false, NULL);
// query for all "checking" Accounts
Set::View vSetResult =
hCache->entrySet(LikeFilter::create(vExtractor, "checking%"));
// iterate the result set printing each matching entry
for (Iterator::Handle hIter = vSetResult->iterator(); hIter->hasNext(); )
{
Map::Entry::View vEntry = cast<Map::Entry::View>(hIter->next());
Account::View vAccount = cast<Account::View>(vEntry->getValue());
std::cout << vAccount << std::endl;
}

As you can see, performing the query and iterating the results is quite trivial. All the real work is in defining extractors and filters. Knowing how to implement custom extractors and filters is important, but keep in mind that you can get quite far with the built-in ones.

Executing aggregators and entry processors

Coherence C++ includes full support for aggregators and entry processors via a native InvocableMap interface, which closely resembles the Java version described in Chapters 5 and 6.

class InvocableMap
: public interface_spec<InvocableMap,
implements<Map> >
{
public:
virtual Object::Holder invoke(Object::View vKey,
EntryProcessor::Handle hAgent);
virtual Map::View invokeAll(Collection::View vCollKeys,
EntryProcessor::Handle hAgent);
virtual Map::View invokeAll(Filter::View vFilter,
EntryProcessor::Handle hAgent);
virtual Object::Holder aggregate(Collection::View vCollKeys,
EntryAggregator::Handle hAgent) const;
virtual Object::Holder aggregate(Filter::View vFilter,
EntryAggregator::Handle hAgent) const;
};

As you can see, the interface allows for explicit key-and-filter based selection of the entries to be processed or aggregated. The operation to perform is expressed as either an EntryProcessor or Aggregator, for which there are a number of built-in implementations. You may also supply your own custom implementations.

The InvocableMap interface is supported by both local and remote caches. By far, the more common case is to use them on remote (clustered) caches. If your custom EntryProcessors and Aggregators will be used against remote caches, you will need to have the corresponding Java version in the classpath of your cache servers. Just as with .NET, if you only intend to use them remotely, your C++ implementations need to only contain state and serialization logic, and can skip the actual processing logic.

Implementing DepositProcessor in C++

The following is a entry processor which can be used to deposit funds into our remote Account cache:

class DepositProcessor
: public class_spec<DepositProcessor,
extends<AbstractProcessor>,
implements<PortableObject> >
{
friend class factory<DepositProcessor>;
// ----- constructors --------------------------------------------
protected:
DepositProcessor()
: m_vMoney(self()), m_vsDescription(self())
{}
DepositProcessor(Managed<Money>::View vMoney, String::View vsDescription)
: m_vMoney(self(), vMoney), m_vsDescription(self(), vsDescription)
{}
// ----- InvocableMap::EntryProcessor interface ------------------
public:
virtual Object::Holder process( InvocableMap::Entry::Handle hEntry) const
{
COH_THROW (UnsupportedOperationException::create());
}
// ----- PortableObject interface --------------------------------
public:
virtual void readExternal(PofReader::Handle hIn)
{
initialize(m_vMoney, cast<Managed<Money>::View>( hIn->readObject(0)));
initialize(m_vsDescription, hIn->readString(1));
}
virtual void writeExternal(PofWriter::Handle hOut) const
{
hOut->writeObject(0, m_vMoney);
hOut->writeString(1, m_vsDescription);
}
// ----- data members --------------------------------------------
protected:
FinalView<Managed<Money> > m_vMoney;
FinalView<String> m_vsDescription;
};
COH_REGISTER_PORTABLE_CLASS(POF_TYPE_DEPOSIT_PROCESSOR, DepositProcessor);

As you can see, this stub implementation contains no processing logic. Its only purpose is to convey the operation type and state when serialized and transmitted to the Java cache servers, where the deserialized Java version will handle the processing work.

Aggregators will follow the same pattern of just being client-side state conveying stubs, and an example is omitted here due to their close similarity with the stub entry processor we've just presented.

Listening for cache events

Just as with Java, C++ clients may register event listeners to be notified when cache entries are inserted, updated, or deleted. These event feeds are extremely useful in building real-time non-polling applications based on cached state.

Cache listeners

Coherence C++ follows the Java-style observer pattern, and event registration is performed against methods declared as part of the ObservableMap interface.

class ObservableMap
: public interface_spec<ObservableMap,
implements<Map> >
{
public:
virtual void addKeyListener(MapListener::Handle hListener,
Object::View vKey, bool fLite);
virtual void removeKeyListener(MapListener::Handle hListener,
Object::View vKey);
virtual void addFilterListener(MapListener::Handle hListener,
Filter::View vFilter = NULL, bool fLite = false);
virtual void removeFilterListener( MapListener::Handle hListener,
Filter::View vFilter = NULL);
};

As you can see, the feature set is the same as that available in the Java version of ObservableMap, including key-and-filter based registrations, and the ability to request lite events. Lite events are free to omit the old and new values in order to avoid the additional resources required to carry them over the network.

When matching events occur, you are notified by a callback on the supplied custom MapListener implementation. The MapListener interface is again similar to the Java version, and the usage is the same as described in Chapter 7,

class MapListener
: public interface_spec<MapListener,
implements<EventListener> >
{
public:
virtual void entryInserted(MapEvent::View vEvent);
virtual void entryUpdated(MapEvent::View vEvent);
virtual void entryDeleted(MapEvent::View vEvent);
};

Let's put together a simple custom listener that prints cache changes to standard output:

class VerboseMapListener
: public class_spec<VerboseMapListener,
extends<Object>,
implements<MapListener> >
{
friend class factory<VerboseMapListener>;
public:
virtual void entryInserted(MapEvent::View vEvent)
{
std::cout << "inserted " << vEvent->getKey() << ", "
<< vEvent->getNewValue() << std::endl;
}
virtual void entryUpdated(MapEvent::View vEvent)
{
std::cout << "updated " << vEvent->getKey() << " from "
<< vEvent->getOldValue() << " to "
<< vEvent->getNewValue() << std::endl;
}
virtual void entryDeleted(MapEvent::View vEvent)
{
std::cout << "deleted " << vEvent->getKey() << std::endl;
}
};

Event listener registration is performed just as in Java:

hCache->addFilterListener(VerboseMapListener::create());

Event notifications occur locally on a dedicated event-dispatching thread associated with the cache. It is thus important to consider pushing any long running listener logic onto application threads so that subsequent events are not blocked or delayed.

Standard type integration

The final Coherence C++ feature we will look at is integration with standard C++ data types. We've already seen that the Coherence-managed String can interoperate with char* and std::string, but there are a number of other type integrations worth considering.

Managed type

Non-managed type

Boolean

bool

Octet

uint8_t, unsigned char

Character16

char16_t, wchar_t

Integer16

int16_t, short

Integer32

int32_t, int

Integer64

int64_t, long long

Float32

float32_t, float

Float64

float64_t, double

String

char*, wchar_t*, std::string, std::wstring

Array<T>

T[], for primitive type T

ObjectArray

Object::Holder[]

RawDateTime

struct tm

Exception

std::exception

For each of these type integrations, the managed type will support some form of assignment from and to the standard type. In most cases, the managed type will be constructable from the standard type, and can be de-referenced and assigned to the standard type:

Integer32::View vInt = Integer32::create(5);
int32_t nInt = *vInt; // assigns 5 to nInt

Another integration point is with std::map, or more specifically, the STL pair associative container concept. It is probably quite clear by this point that the Coherence caches do not operate based on std::map, but rather based on coherence::util::Map interface, which mimics java.util.Map. For those who prefer the feel of std::map, or those replacing an existing std::map-based local cache, Coherence includes an adapter to make any Coherence map or cache implementation usable through a std::map-style API.

The adapter class boxing_map is an implementation of the std::map (pair associative container) concept, which delegates to any coherence::util::Map implementation. This includes doing the work of converting the keys and values back to their non-managed C++ types, making for an even stronger traditional C++ feel.

As an example, let's create a boxing_map around our Account cache, and re-write our original cache access code:

boxing_map<Integer64, Managed<Account> >
cache(CacheFactory::getCache("accounts"));
// construct plain old Account object
Account account(32105, "checking", Money(7374, 10, "USD"), 55, 62409);
// cache account
cache[32105] = account;
// retrieve the cached value
Account accountResult = cache[32105];

As you can see, other than the declaration, all the other statements are just as they would be for std::map. The boxing_map includes all the standard operators and methods you'd expect from std::map. This allows for some interesting combinations. For instance, you can make use of STL algorithms to operate on the cache, for example, copying std::map into a cache:

std::map<int64_t, Account> mapBatch;
// fill up mapBatch with records
// ...
// use std::copy to transfer them to the cache
std::copy(mapBatch.begin(), mapBatch.end(),
std::inserter(cache, cache.begin()));

While the ability to access caches as std::maps is useful, it is also limited. Most of the advanced features of Coherence caches are unavailable because the std::map API does not have corresponding concepts. Ultimately, the boxing_map is really intended for applications that would primarily access the cache in a get/put style, leaving more advanced cache usage to be accessible only through the Coherence cache interfaces.

Summary

This concludes our foray into the land of Coherence C++.

Start playing with the APIs, dig deep, and look for other features we've discussed for the Java version. In general, you'll find that they exist and are in the same basic form. There is a lot there to make use of, so have fun!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.172.93