Call us: +1-415-738-4000

BigMemory .NET Client API

Cross-Language API

This section covers connecting to, working with, and searching your BigMemory data. For the complete class library documentation, refer to the API documentation in the /apis/csharp/apidoc-csharp.zip directory of the kit.

Connecting with the CL Connector

Your application will communicate with the CL Connector via the ICacheManager interface.

Begin by creating a Configuration object. For a deployment where the BigMemory Client is located on the same machine as the CL Connector, use a configuration with the Nirvana Shared Memory (SHM) transport:

Configuration cfg=new Configuration("/dev/shm", 16));

For a deployment where the BigMemory Client and the CL Connector are located on different machines, use a configuration with the Nirvana socket transport. For example, here we create a configuration that connects to the local host at the default port, uses Nirvana socket transport, and provisions a pool of 16 connections to share:

Configuration cfg=new Configuration("localhost", Constants.defaultPort, TransportType.NIRVANA, 16);

Notice that configurations with different transport types require different parameters:

  • Nirvana Shared Memory (SHM)Configuration("SHMLocation", clientPoolSize) — generally used when the BigMemory Client is located on the same machine as the CL Connector.
  • Nirvana socketConfiguration("hostName", portNumber, TransportType.NIRVANA, clientPoolSize) — used when the CL Connector and the BigMemory Client are located on different machines.

Once you have your Configuration instance ready, create an ICacheManager using the Xplatform.CreateCacheManager static method:

ICacheManager cachemanager = XPlatform.CreateCacheManager(cfg);

Shutting the connection down

The ICacheManager needs to be closed in order for it to release all resources (like connections). Close it by invoking the Close() method on it:

cachemanager.Close();

Accessing a Cache

Once you’ve obtained a reference to your ICacheManager, it is now connected to your CL Connector and will let you access any Cache known by the CL Connector.

To retrieve a thread-safe instance of a Cache, you will need its name, the type of the key, the value, and a serializer for those types.

Serializers

The protocol recognizes the following types:

  • bool: A boolean value (true or false), one byte
  • byte: A signed byte
  • i16: A 16-bit signed integer
  • i32: A 32-bit signed integer
  • i64: A 64-bit signed integer
  • double: A 64-bit floating point number
  • byte[]: An array of bytes
  • string: Encoding agnostic text or binary string

If you want to send complex types to the CL Connector, you will have to provide a serializer that can serialize your complex key and value types for a Cache.

Say we have the following Class, which is our Value type:

public class User
{
    public long Id { get; set; }
    public string Login { get; set; }
    public string Email { get; set; }
}

We store these keyed to the login, as a string. We now need to provide a serializer like this:

public class UserCacheSerializer : ICacheSerializer<string,User>
 {

    public Value SerializeKey(string key)
    {
        Value v = new Value();
        v.StringValue = key;
        return v;
    }

    public string DeserializeKey(Value serializedKey)
    {
        return serializedKey.StringValue;
    }

    public StoredValue SerializeValue(User objectValue)
    {
        Dictionary<String, Value> nvpairs = new Dictionary<string, Value>();

        Value idval = ValueHelper.NewValue(objectValue.Id);
        nvpairs["id"] = idval;

        Value emailval = ValueHelper.NewValue(objectValue.Email);
        nvpairs["email"] = emailval;

        StoredValue sv = new StoredValue();
        sv.Value = ValueHelper.NewValue(Encoding.Unicode.GetBytes(objectValue.Login));
        sv.Nvpairs = nvpairs;
        return sv;

    }

    public User DeserializeValue(StoredValue serializedValue)
    {
        User user = new User();
        user.Login = Encoding.Unicode.GetString(serializedValue.Value.BinaryValue);
        user.Id = serializedValue.Nvpairs["id"].LongValue;
        user.Email = serializedValue.Nvpairs["email"].StringValue;
        return user;
    }
}

Note that both serialize and deserialize methods (whether for key or value) are kept symmetric.

StoredValue is composed of the actual Value (in this example, the byte array representation of the user’s login). StoredValue can hold entire object graphs serialized with whatever serialization strategies fits your needs. It also holds an arbitrary amount of name/value pairs. The names are String, while the values are of type Value. You can use these name-value pairs to store whatever you want. Note that these name-value pairs also become indexable and searchable using the search API.

A byte array is not indexable, however it can be used within name-value pairs. In the above example, both user id and email are stored as byte arrays. Even though byte arrays are not indexable, search can be enabled on the attributes. Since we use the login as the key in our use case, storing that as a binary representation within the “default unindexed value” is good enough.

Special handling for DateTime objects

C# captures date information with more precision than Java does, which means the string representation has parts that Java is not prepared to parse. There are two parts to handling this. First, use the IsoDateTimeConverter for the C# side serialization and deserialization code. Second, trim C# DateTime objects to millisecond granularity. You can do this via the Terracotta.Ehcache.Utilities.DateUtility.TrimToMillisGranularity(DateTime dt) method. It returns a new DateTime object trimmed appropriately. Alternatively, you can use ValueHelper.newValue(..) on a DateTime object to do the trimming for you.

Key-based store and retrieve

Now that we have defined an ICacheSerializer for our cache, we can retrieve the cache from the ICacheManager. The example below assumes that we have defined a cache named “userCache” within the ehcache.xml file used to configure the CL Connector.

// create a serializer
ICacheSerializer<string, User> serializer = new UserCacheSerializer();

// get a cache 
ICache<string, User> users = cachemanager.GetCache("userCache", serializer);

// make a user
User user1 = new User();
user1.Id = 1;
user1.Email = "someone@somewhere";
user1.Login = "secretpasswd";

// put the user
users.Put(user1.Login, user1, ConsistencyType.STRONG);

The following is an example if you were using the BSON code sample for .NET (Recipe2 is the class defining the domain object, and Recipe2BsonSerializer() is the serializer class).

ICache<string, Recipe2> cache = cacheManager.GetCache("TestCache", new Recipe2BsonSerializer());
cache.Put(rec.Name, rec, ConsistencyType.STRONG);

The ConsistencyType attribute sets the consistency, strong or eventual, with which you expect the CL Connector to execute an operation. For more information, refer to the Consistency section below.

To retrieve by key, invoke the get method:

 User someUser = userCache.Get(someLogin, ConsistencyType.EVENTUAL);

To retrieve in C#:

Recipe2 rec = (Recipe2)cache.Get(args[1], ConsistencyType.EVENTUAL);

Compare and Swap (CAS) Operations

The following CAS operations are provided in C#:

  • PutIfAbsent(TKey, TValue) — Puts the specified key/value pair into the cache only if the key has no currently assigned value. Unlike Put, which can replace an existing key/value pair, PutIfAbsent creates the key/value pair only if it is not present in the cache.

  • Remove(TKey, TValue) — Conditionally removes the specified key, if it is mapped to the specified value.

  • Replace(TKey, TValue) — Maps the specified key to the specified value, if the key is currently mapped to some value.

  • Replace(TKey, OldValue, NewValue) — Conditionally replaces the specified key's old value with the new value, if the currently existing value matches the old value.

For more information about atomic operations and cache consistency, refer to the Consistency section below.

This section provides an overview of how to do cross-language searches of PNF data stored within BigMemory. More detailed information may be found on the BigMemory Search Setup page.

In order to search BigMemory data, you first need to add the <searchable/> tag to the cache configuration in your ehcache.xml file.

<ehcache>
(...)
  <cache name="userCache">
  (...)
    <searchable/>
  </cache>
</ehcache>

This configuration will scan all keys and values in the cache and, if they are of supported search types, add them as search attributes. Values of the Name/Value pairs in the PNF’s StoredValue sent to the CL Connector, other than of type byte[], can all be indexed and made searchable. (Continuing with the example of the previous section, this would mean id and email.)

You can also add search attributes using the provided attributeExtractor class, for example:

<ehcache>
(...)
  <cache name="userCache">
  (...)
    <searchable>
      <searchAttribute name="email" class="net.sf.ehcache.xplatform.search.Indexer"/>
    </searchable>
  </cache>
</ehcache>

To perform a query, use the Cache.query(String) method, providing a BigMemory SQL query string, for example:

SearchResults<string, User> result = userCache.Query("select * from userCache where email ilike '%@terracottatech.com'");

For more information about how to construct BigMemory SQL queries, refer to the BigMemory SQL Queries page.

Note: Java (POJO) data stored within BigMemory can also be searched using the native Ehcache Search API, which is documented on the BigMemory Max Search API page.

Consistency

Consistency is set in the ehcache.xml configuration file, and it has to do with how the cache you connect to is clustered on the CL Connector. A cache clustered using strong consistency will only be able to have strong operations performed on it, meaning that changes made by any node in the cluster are visible instantly. Atomic operations (for example, putIfAbsent) are always performed in the strong mode. A cache clustered using eventual consistency, on the other hand, will be able to perform both strong and eventual operations, and changes made by any node are visible to other nodes eventually. To understand consistency at the Terracotta clustering level, refer to the BigMemory documentation.

In the Cross-Language API, the ConsistencyType attribute sets the consistency, strong or eventual, with which you expect the CL Connector to execute an operation. All operations on a cache that can be performed using either strong or eventual consistency take ConsistencyType as an additional parameter. Other than a few operations such as atomic ops, RemoveAll, and queries, most regular cache operations accept this last parameter.

When set to STRONG, data in the cache remains consistent across the cluster at all times. This mode guarantees that a read gets an updated value only after all write operations to that value are completed, and it guarantees that each put operation is in a separate transaction. When set to EVENTUAL, reads without locks are allowed, and read/write performance is substantially boosted, but at the cost of potentially having an inconsistent cache for brief periods of time.

If you try to execute an eventually consistent operation (for example, cache.get(key, EVENTUAL)) on a strong cache, you will get an exception. Executing a strongly consistent operations on an eventual cache is allowed but will result in locks being used on the CL Connector, and your method will return only when all of the steps are finished (acquiring the appropriate lock, performing the operation, and releasing the lock).