Initalizing an instance of RiakClient

Everything starts with creating an instance of RiakClient. RiakClient takes two core arguments, namely a reference to an Akka ActorSystem and the location of the Riak node you want to interact with.

If your app is not based on Akka and therefor has no access to an initialized actor system, you can leave off the ActorSystem argument and one will be created for you internally. The location of a riak node can be specified as either an HTTP url pointing to root of your Riak node or a combination of a hostname and a port number.

// Specifying an existing ActorSystem
val client = RiakClient(actorSystem, "localhost", 80)

// or
val client = RiakClient(actorSystem, "http://riak-node.com/")

// Using an internal ActorSystem
val client = RiakClient("localhost", 80)

// or
val client = RiakClient("http://riak-node.com/")

It is important to note that internally RiakClient uses an Akka extension to ensure there can be only one instance of RiakClient per actor system. This means that you can "create" as many instances of RiakClient as you want because under the surface they all point at one single instance.

RiakValue and RiakMeta

Before we can fetch and store values, it might be a good idea to talk about how those values are represented.

RiakValue represents the raw, serialized value stored in Riak and all its associated meta data, such as its content type, its vclock, its etag, its last modified timestamp its indexes, etc. Most interactions with Riak involve dealing with instances of RiakValue in one way or another.

Since dealing with raw, untyped data is usually not what you want when programming in Scala, any RiakValue can be transformed into an instance of RiakMeta[T], which represents the same Riak meta data as RiakValue but now the raw data has been deserialized into some type T. This is accomplished by calling its asMeta[T] method.

val raw: RiakValue = ...

val meta: RiakMeta[Myclass] = raw.asMeta[MyClass]

The reverse transformation is just as simple. Just call the toRiakValue method on any instance of RiakMeta.

val meta: RiakMeta[Myclass] = raw.asMeta[MyClass]

val raw: RiakValue = meta.toRiakValue

If you just want access to the deserialized T without any of the Riak meta data, you can call the as[T] method on RiakValue:

val raw: RiakValue = ...

val myClass: Myclass = raw.as[MyClass]

This probably all seems pretty abstract to you. For a more integrated example of how to work with RiakValue and RiakMeta, have a look at the Examples section.

Serialization

Of course, these transformations between raw serialized data and typed deserialized data don't happen by themselves. These transformations require compatible instances of the RiakSerializer[T] and RiakDeserializer[T] type classes to be available in implicit scope. Let's have a look at their definitions:

/**
 * A RiakSerializer is a type class trait for implementing serialization from some
 * type T to a Tuple2 of raw data (a String) and a ContentType.
 */
@implicitNotFound(msg = "Cannot find RiakSerializer type class for ${T}")
trait RiakSerializer[T] {
  def serialize(t: T): (String, ContentType)
}

/**
 * A RiakDeserializer is a type class trait for implementing deserialization from some
 * raw data (a String) and a ContentType to a type T.
 */
@implicitNotFound(msg = "Cannot find RiakDeserializer type class for ${T}")
trait RiakDeserializer[T] {
  def deserialize(data: String, contentType: ContentType): T
}

riak-scala-client uses String as the lowest level encoding, which means binary formats are not currently supported. This was a design choice made to keep the API as simple as possible. Please let us know if you would like support for binary formats!

riak-scala-client comes with out-of-the-box support for (de)serializing raw Strings (i.e no serialization at all, using text/plain as the content type) and any case classes with an associated spray-json RootJsonFormat (using application/json). These out-of-the-box implementations will be used by default if you don't create your own serializers (and your classes fit the criteria described above) See the Examples section for examples using both builtin and custom serialization.

Buckets

Just like in Riak, buckets in riak-scala-client are just a way to namespace your keys. You can get a reference to a particular bucket simply by calling the bucket method of RiakClient, resulting in an instance of RiakBucket.

val client = RiakClient(actorSystem, "http://riak-node.com/")

val bucket = client.bucket("my-first-bucket")

Most functionality of riak-scala-client is exposed as methods on a RiakBucket, as you will see below.

Fetching Values

As you would expect, fetching data from Riak is very simple indeed. Just call the fetch method on any bucket with a key fo your choice:

val bucket = client.bucket("my-first-bucket")

val value: Future[Option[RiakValue]] = bucket.fetch("some-awesome-key")

As you can see, the return type of fetch is Future[Option[RiakValue]]. All operations in riak-scala-client that interact with Riak are non-blocking so all of those operations will result in some type of Future being produced. The Examples section has lots of examples of how to interact with Futures.

Obviously not all keys will be bound to a value in Riak so the Future returned by fetch wraps an Option[RiakValue].

Conflict resolution during fetch and fetching with secondary indexes will be discussed below.

Storing Values

Storing values in Riak is almost as easy as fetching them. The most basic store operation is one that takes a (String) key and a RiakValue and returns a Future[Unit].

val value: RiakValue = ...

val result: Future[Unit] = bucket.store("some-awesome-key", value)

This is basically a fire-and-forget operation but you can use the returned Future to handle any errors that might occur while riak-scala-client communicates with Riak. If you want to have access to the value you just stored, or more probably to its meta data (i.e. to make sure you are working a value based on the latest vclock), you can use the storeAndFetch method, which will perform a Riak HTTP store operation using the returnbody=true query parameter.

val value: RiakValue = ...

val result: Future[RiakValue] = bucket.storeAndFetch("some-awesome-key", value)

Working with raw RiakValue instances is fine when you get them returned from a fetch operation but when you want to store data it is often more convenient to work with your own domain classes. To that end you can also store any type T for which there is a RiakSerializer[T] and a RiakIndexer[T] in implicit scope.

val value: MyClass = ...

// this only compiles if both a RiakSerializer[MyClass] and a
// RiakIndexer[MyClass] are implicitly available.
val result: Future[RiakValue] = bucket.storeAndFetch("some-awesome-key", value)

As long as the value is either a String, a class associated with a spray-json RootJsonFormat[T], or a class associated with a custom RiakSerializer[T], it will be automatically serialized and stored. The Examples section has more details about how to use the builtin serializers or how to define your own ones.

Don't worry about the indexer part for now since there is always a default indexer in scope that will not index anything. See the section on secondary indexes below for more information.

Lastly, you can also store any instance of RiakMeta[T] directly without having to convert it to a RiakValue yourself.

Deleting Values

Deleting values from Riak is probably the simplest operation you can perform. Just pass the key to the delete method.

val result: Future[Unit] = bucket.delete("my-less-awesome-key")

As with the fire-and-forget version of store, you can use the returned Future to handle any possible errors gracefully.

Conflict Resolution

If you decide to turn on support for siblings for one (or more) of your buckets, which you can do using the allow_mult bucket property (also available as allowSiblings), then any fetch or storeAndFetch can result in a conflict to be resolved. riak-scala-client allows you to solve these conflicts yourself by specifying a RiakConflictsResolver when getting a reference to a bucket.

val bucket = client.bucket("stuff", StuffConflictsResolver)

// or
val bucket = client.bucket("stuff", resolver = StuffConflictsResolver)

Conflict resolvers are usually implemented as (case) objects since they are stateless. To create a custom resolver you will need to extend from the RiakConflictsResolver trait and implement the resolve method. RiakConflictsResolver is defined as follows:

trait RiakConflictResolver {
  def resolve(values: Set[RiakValue]): RiakValue
}

Your custom conflict resolver will be presented with a Set of RiakValue instances and its job is to produce one RiakValue instance, which could be one from the set or a new one created based on some combination of the values in the set.

If the bucket you are working with has sibling support turned on (i.e. allow_mult == true), you should always specify a conflict resolver. Failing to do so will activate the default no-op resolver, which will throw an exception to remind you to specify a real resolver.

The Examples section contains at least one example of a custom resolver and you can find more examples in the riak-scala-client unit tests.

Secondary Indexes (2i)

riak-scala-client fully supports Riak secondary indexes. Every RiakValue has a set of RiakIndex instances representing the secondary indexes it should be stored with. There are also a number of extra versions of the fetch method for fetching data by either a single index or an index range. The result of such index fetches is always a Future[List[RiakValue]] where riak-scala-client takes care of fetching the individual values based on the keys returned by the HTTP index fetch.

Using a Future[List[RiakValue]] here is not ideal since the list of values might be very big and might not even fit into available memory. You also have to wait (in a matter of speaking) until all the values have been retrieved before you can do anything with them. The next version of riak-scala-client will very probably use Play Iteratees to reimplement these methods (or to create streaming versions of them).

That being said, let's look at some examples:

val values: Future[List[RiakValue]] = bucket.fetch("skill", "awesomeness")

val values: Future[List[RiakValue]] = bucket.fetch("answer", 42)

val values: Future[List[RiakValue]] = bucket.fetch("timestamp", "201301010000", "201302010000")

val values: Future[List[RiakValue]] = bucket.fetch("size", 10000, 40000)

riak-scala-client will add the appropriate type suffix (i.e. "_bin" or "_int") to the name you specify for the index so you don't have to. All index names and index values will also be properly URL encoded so your index names and values can contain non-ascii values.

Just like you can create a custom (de)serializer type class for your domain class, you can also create a custom indexer by providing an implicit implementation of the RiakIndexer[T] trait, which looks like this:

@implicitNotFound(msg = "Cannot find RiakIndex type class for ${T}")
trait RiakIndexer[T] {
  def index(t: T): Set[RiakIndex]
}

riak-scala-client provides a default implementation of RiakIndexer[T] for any T that simply creates an empty set of indexes. This implementation is available from the lowest possible implicit scopes so any custom implementation you make implicitly available will always override it.

Bucket Properties

Next to the obvious methods for fetching, storing, and deleting values, RiakBucket also exposes methods for getting and setting common bucket properties. Some examples:

// Get all bucket properties at once
val props: Future[RiakBucketProperties] = bucket.properties

// get an individual property
val allowSiblings: Future[Boolean] = bucket.allowSiblings

See the Scaladocs for RiakBucket and RiakBucketProperties for the full set of supported bucket properties.