Updating Objects
Using Causal Context
If an object already exists under a certain key and you want to write a new object to that key, Riak needs to know what to do, especially if multiple writes are happening at the same time. Which of the objects being written should be deemed correct? These kinds of scenarios can arise quite frequently in distributed, eventually consistent systems.
Riak decides which object to choose in case of conflict using causal context. These objects track the causal history of objects. They are attached to all Riak objects as metadata, and they are not readable by humans. They may sound complex—and they are fairly complex behind the scenes—but using them in your application is very simple.
Using causal context in an update would involve the following steps;
- Fetch the object
- Modify the object’s value (without modifying the fetched context object
- Write the new object to Riak
Step 2 is the most important here. All of Riak’s official Riak clients enable you to modify an object’s value without modifying its causal context. Although a more detailed tutorial on context objects and object updates can be found in Conflict Resolution, we’ll walk you through a basic example here.
Let’s say that the current NBA champion is the Washington Generals.
We’ve stored that data in Riak under the key champion
in the bucket
nba
, which bears the bucket type sports
. The value of the object is
a simple text snippet that says Washington Generals
.
But one day the Harlem Globetrotters enter the league and dethrone the
hapless Generals (forever, as it turns out). Because we want our Riak
database to reflect this new development in the league, we want to make
a new write to the champion
key. Let’s read the object stored there
and modify the value.
Location currentChampion = new Location(new Namespace("sports", "nba"), "champion");
FetchValue fetch = new FetchValue.Builder(currentChampion)
.build();
FetchValue.Response response = client.execute(fetch);
RiakObject obj = response.getValue(RiakObject.class);
obj.setValue(BinaryValue.create("Harlem Globetrotters"))
bucket = client.bucket_type('sports').bucket('nba')
obj = bucket.get('champion')
obj.raw_data = 'Harlem Globetrotters'
obj.store
$location = new \Riak\Riak\Location('champion', new \Riak\Riak\Bucket('nba', 'sports'));
$object = (new \Riak\Riak\Command\Builder\FetchObject($riak))
->withLocation($location)
->build()
->execute()
->getObject();
$object->setData('Harlem Globetrotters');
(new \Riak\Riak\Command\Builder\StoreObject($riak))
->withLocation($location)
->withObject($object)
->build()
->execute();
bucket = client.bucket_type('sports').bucket('nba')
obj = bucket.get('champion')
obj.data = 'Harlem Globetrotters'
var id = new RiakObjectId("sports", "nba", "champion");
var obj = new RiakObject(id, "Washington Generals",
RiakConstants.ContentTypes.TextPlain);
var rslt = client.Put(obj);
rslt = client.Get(id);
obj = rslt.Value;
obj.SetObject("Harlem Globetrotters",
RiakConstants.ContentTypes.TextPlain);
rslt = client.Put(obj);
var riakObj = new Riak.Commands.KV.RiakObject();
riakObj.setContentType('text/plain');
riakObj.setValue('Washington Generals');
var options = {
bucketType: 'sports', bucket: 'nba', key: 'champion',
value: riakObj
};
client.storeValue(options, function (err, rslt) {
if (err) {
throw new Error(err);
}
delete options.value;
client.fetchValue(options, function (err, rslt) {
if (err) {
throw new Error(err);
}
var fetchedObj = rslt.values.shift();
fetchedObj.setValue('Harlem Globetrotters');
options.value = fetchedObj;
options.returnBody = true;
client.storeValue(options, function (err, rslt) {
if (err) {
throw new Error(err);
}
var updatedObj = rslt.values.shift();
logger.info("champion: %s", updatedObj.value.toString('utf8'));
});
});
});
%% In the Erlang client, you cannot view a context objectdirectly, but it
%% will be included in the output when you fetch an object:
{ok, Obj} = riakc_pb_socket:get(Pid,
{<<"sports">>, <<"nba">>},
<<"champion">>),
UpdatedObj = riakc_obj:update_value(Obj, <<"Harlem Globetrotters">>),
{ok, NewestObj} = riakc_pb_socket:put(Pid, UpdatedObj, [return_body]).
obj := &riak.Object{
ContentType: "text/plain",
Charset: "utf-8",
ContentEncoding: "utf-8",
Value: []byte("Washington Generals"),
}
cmd, err := riak.NewStoreValueCommandBuilder().
WithBucketType("sports").
WithBucket("nba").
WithKey("champion").
WithContent(obj).
WithReturnBody(true).
Build()
if err != nil {
fmt.Println(err.Error())
return
}
if err := cluster.Execute(cmd); err != nil {
fmt.Println(err.Error())
return
}
svc := cmd.(*riak.StoreValueCommand)
rsp := svc.Response
obj = rsp.Values[0]
obj.Value = []byte("Harlem Globetrotters")
cmd, err = riak.NewStoreValueCommandBuilder().
WithBucketType("sports").
WithBucket("nba").
WithKey("champion").
WithContent(obj).
WithReturnBody(true).
Build()
if err != nil {
fmt.Println(err.Error())
return
}
if err := cluster.Execute(cmd); err != nil {
fmt.Println(err.Error())
return
}
svc = cmd.(*riak.StoreValueCommand)
rsp = svc.Response
obj = rsp.Values[0]
fmt.Printf("champion: %v", string(obj.Value))
# When using curl, the context object is attached to the X-Riak-Vclock header
curl -i http://localhost:8098/types/sports/buckets/nba/keys/champion
# In the resulting output, the header will look something like this:
X-Riak-Vclock: a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
# When performing a write to the same key, that same header needs to
# accompany the write for Riak to be able to use the context object
In the samples above, we didn’t need to actually interact with the context object, as retaining and passing along the context object was accomplished automatically by the client. If, however, you do need access to an object’s context, the clients enable you to fetch it from the object:
// Using the RiakObject obj from above:
Vclock vClock = obj.getVclock();
System.out.println(vClock.asString());
// The context object will look something like this:
// a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
# Using the RObject obj from above:
obj.vclock
# The context object will look something like this:
# a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
# Using the RObject obj from above:
echo $object->getVclock(); // a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
# Using the RiakObject obj from above:
obj.vclock
# The context object will look something like this:
# a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
// Using the RiakObject obj from above:
var vclock = result.Value.VectorClock;
Console.WriteLine(Convert.ToBase64String(vclock));
// The output will look something like this:
// a85hYGBgzGDKBVIcWu/1S4OVPaIymBIZ81gZbskuOMOXBQA=
// Using the RiakObject fetchedObj from above:
var fetchedObj = rslt.values.shift();
logger.info("vclock: %s", fetchedObj.getVClock().toString('base64'));
// The output will look something like this:
// vclock: a85hYGBgymDKBVIcR4M2cov1HeHKYEpkymNlsE2cfo4PKjXXjuOU+FHdWqAUM1CqECSVBQA=
%% Using the Obj object from above:
riakc_obj:vclock(Obj).
%% The context object will look something like this in the Erlang shell:
%% <<107,206,97,96,96,96,204,96,202,5,82,28,202,156,255,126,
%% 6,175,157,255,57,131,41,145,49,143,149,225,240,...>>
svc := cmd.(*riak.StoreValueCommand)
rsp := svc.Response
fmt.Println(rsp.VClock)
// Output:
// X3hNXFq3ythUqvvrG9eJEGbUyLS
The Object Update Cycle
If you decide that your application requires mutable data in Riak, we recommend that you:
- avoid high-frequency object updates to the same key (i.e. multiple updates per second for long periods of time), as this will degrade Riak performance; and that you
- follow a read-modify-write cycle when performing updates.
That cycle looks something like this:
- Read the object from Riak. This step is important for updates because this enables you to fetch the object’s causal context, which is the information that Riak uses to make decisions about which object values are most recent (this is especially useful for objects that are frequently updated). This context object needs to be passed back to Riak when you update the object. This step is handled for you by Riak’s client libraries as long as you perform a read prior to an update. In addition, if you have chosen to allow Riak to generate siblings (which we recommend), you should resolve sibling conflicts upon read if they exist. For more on this, please see our documentation on conflict resolution, along with examples from our official client libraries:
- Modify the object on the application side.
- Write the new, modified object to Riak. Because you read the object first, Riak will receive the object’s causal context metadata. Remember that this happens automatically.
In general, you should read an object before modifying it. Think of it
as performing a GET
prior to any PUT
when interacting with a REST
API.
Note on strong consistency
If you are using Riak’s strong consistency feature, it is not only desirable but also necessary to use the read/modify/write cycle explained in the section above. If you attempt to update an object without fetching the object first, your update operation will necessarily fail. More information can be found in the strong consistency documentation.
Updating Deleted Objects
You should use the read-modify-write cycle explained above at all times, even if you’re updating deleted objects. The reasons for that can be found in our documentation on tombstones.
There are some modifications that you may need to make if you are
updating objects that may have been deleted previously. If you are using
the Java client, an explanation and examples are given in the
Java-specific section below. If
you are using the Python or Erlang clients, causal context for deleted
objects will be handled automatically. If you are using the Ruby client,
you will need to explicitly set the deletedvclock
parameter to true
when reading an object, like so:
bucket = client.bucket('fruits')
obj = bucket.get('banana', deletedvclock: true)
Example Update
In this section, we’ll provide an update example for Riak’s official Ruby, Python, .NET, Node.js, Erlang and Go clients. Because updates with the official Java client functions somewhat differently, those examples can be found in the section below.
For our example, imagine that you are storing information about NFL head
coaches in the bucket coaches
, which will bear the bucket type
siblings
, which sets allow_mult
to true
. The key for each object
is the name of the team, e.g. giants
, broncos
, etc. Each object will
consist of the name of the coach in plain text. Here’s an example of
creating and storing such an object:
bucket = client.bucket('coaches')
obj = bucket.get_or_new('seahawks', type: 'siblings')
obj.content_type = 'text/plain'
obj.raw_data = 'Pete Carroll'
obj.store
$location = new \Riak\Riak\Location('seahawks', new \Riak\Riak\Bucket('coaches', 'siblings'));
$response = (new \Riak\Riak\Command\Builder\FetchObject($riak))
->atLocation($location)
->build()
->execute();
if ($response->isSuccess()) {
$object = $response->getObject();
$object->setData('Pete Carroll');
} else {
$object = new \Riak\Riak\Object('Pete Carroll', 'text/plain');
}
(new \Riak\Riak\Command\Builder\StoreObject($riak))
->withObject($object)
->atLocation($location)
->build()
->execute();
bucket = client.bucket_type('siblings').bucket('coaches')
obj = RiakObject(client, bucket, 'seahawks')
obj.content_type = 'text/plain'
obj.data = 'Pete Carroll'
obj.store()
var id = new RiakObjectId("siblings", "coaches", "seahawks");
var obj = new RiakObject(id, "Pete Carroll",
RiakConstants.ContentTypes.TextPlain);
var rslt = client.Put(obj);
var riakObj = new Riak.Commands.KV.RiakObject();
riakObj.setContentType('text/plain');
riakObj.setBucketType('siblings');
riakObj.setBucket('coaches');
riakObj.setKey('seahawks');
riakObj.setValue('Pete Carroll');
client.storeValue({ value: riakObj }, function (err, rslt) {
if (err) {
throw new Error(err);
} else {
logger.info('Stored Pete Carroll');
}
});
Obj = riakc_obj:new({<<"siblings">>, <<"coaches">>},
<<"seahawks">>,
<<"Pete Carroll">>,
<<"text/plain">>).
riakc_pb_socket:put(Pid, Obj).
obj := &riak.Object{
ContentType: "text/plain",
Charset: "utf-8",
ContentEncoding: "utf-8",
Value: []byte("Pete Carroll"),
}
cmd, err := riak.NewStoreValueCommandBuilder().
WithBucketType("siblings").
WithBucket("coaches").
WithKey("seahawks").
WithContent(obj).
Build()
if err != nil {
fmt.Println(err.Error())
return
}
if err := cluster.Execute(cmd); err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Stored Pete Carroll")
Every once in a while, though, head coaches change in the NFL, which means that our data would need to be updated. Below is an example function for updating such objects:
def update_coach(team, new_coach)
bucket = client.bucket('coaches')
# The read phase
obj = bucket.get_or_new(team, type: 'siblings')
# The modify phase
obj.data = new_coach
# The write phase
obj.store
end
# Example usage
update_coach('packers', 'Vince Lombardi')
function update_coach($team, $coach) {
$location = new \Riak\Riak\Location('seahawks', new \Riak\Riak\Bucket('coaches', 'siblings'));
$response = (new \Riak\Riak\Command\Builder\FetchObject($riak))
->atLocation($location)
->build()
->execute();
if ($response->isSuccess()) {
$object = $response->getObject();
$object->setData('Pete Carroll');
} else {
$object = new \Riak\Riak\Object('Pete Carroll', 'text/plain');
}
$response = (new \Riak\Riak\Command\Builder\StoreObject($riak))
->withObject($object)
->atLocation($location)
->build()
->execute();
return $response->isSuccess();
}
echo update_coach('packers', 'Vince Lombardi'); // true
def update_coach(team, new_coach):
bucket = client.bucket_type('siblings').bucket('coaches')
# The read phase
obj = bucket.get(team)
# The modify phase
obj.data = new_coach
# The write phase
obj.store()
# Example usage
update_coach('packers', 'Vince Lombardi')
private void UpdateCoach(string team, string newCoach)
{
var id = new RiakObjectId("siblings", "coaches", team);
var getResult = client.Get(id);
RiakObject obj = getResult.Value;
obj.SetObject<string>(newCoach, RiakConstants.ContentTypes.TextPlain);
client.Put(obj);
}
function update_coach(team, newCoach) {
client.fetchValue({
bucketType: 'siblings', bucket: 'coaches', key: team
}, function (err, rslt) {
if (err) {
throw new Error(err);
}
var riakObj = rslt.values.shift();
riakObj.setValue(newCoach);
client.storeValue({ value: riakObj }, function (err, rslt) {
if (err) {
throw new Error(err);
}
});
});
}
update_coach(team, new_coach) ->
{ok, Obj} = riakc_pb_socket:get(Pid,
{<<"siblings">>, <<"coaches">>},
<<team>>),
ModifiedObj = riakc_obj:update_value(Obj, <<new_coach>>),
riakc_pb_socket:put(Pid, ModifiedObj).
%% Example usage
update_coach('packers', 'Vince Lombardi')
func updateCoach(cluster *riak.Cluster, team, newCoach string) error {
var cmd riak.Command
var err error
cmd, err = riak.NewFetchValueCommandBuilder().
WithBucketType("siblings").
WithBucket("coaches").
WithKey(team).
Build()
if err != nil {
return err
}
if err := cluster.Execute(cmd); err != nil {
return err
}
fvc := cmd.(*riak.FetchValueCommand)
obj := fvc.Response.Values[0]
obj.Value = []byte(newCoach)
cmd, err = riak.NewStoreValueCommandBuilder().
WithBucketType("siblings").
WithBucket("coaches").
WithKey(team).
WithContent(obj).
Build()
if err != nil {
return err
}
if err := cluster.Execute(cmd); err != nil {
return err
}
return nil
}
In the example above, you can see the three steps in action: first, the object is read, which automatically fetches the object’s causal context; then the object is modified, i.e. the object’s value is set to the name of the new coach; and finally the object is written back to Riak.
Object Update Anti-patterns
The most important thing to bear in mind when updating objects is this: you should always read an object prior to updating it unless you are certain that no object is stored there. If you are storing sensor data in Riak and using timestamps as keys, for example, then you can be sure that keys are not repeated. In that case, making writes to Riak without first reading the object is fine. If you’re not certain, however, then we recommend always reading the object first.
Java Client Example
As with the other official clients, object updates using the Java client
will automatically fetch the object’s causal context metadata, modify
the object, and then write the modified value back to Riak. You can
update object values by creating your own UpdateValue
operations that
extend the abstract class Update<T>
. An UpdateValue
operation must
have an apply
method that returns a new T
. In our case, the data
class that we’re dealing with is User
. First, let’s create a very
basic User
class:
public class User {
public String username;
public List<String> hobbies;
public User(String username, List<String> hobbies) {
this.name = username;
this.hobbies = hobbies;
}
}
In the example below, we’ll create an update value operation called
UpdateUserName
:
import com.basho.riak.client.api.commands.kv.UpdateValue.Update;
public class UpdateUserName extends Update<User> {
@Override
public User apply(User original) {
// update logic goes here
}
}
In the example above, we didn’t specify any actual update logic. Let’s
change that by creating an UpdateValue
operation that changes a User
object’s name
parameter:
public class UpdateUserName extends Update<User> {
private String newUsername;
public UpdateUserName(String newUsername) {
this.newUsername = newUsername;
}
@Override
public User apply(User original) {
original.username = newUsername;
return original;
}
}
Now, let’s put our UpdateUserName
operation into effect. In the
example below, we’ll change a User
object’s username
from whatever
it currently is to cliffhuxtable1986
:
import com.basho.riak.client.api.commands.kv.FetchValue;
Location location = new Location(...);
UpdateValue updateOp = new UpdateValue.Builder(location)
.withFetchOption(FetchValue.Option.DELETED_VCLOCK, true)
.withUpdate(new UpdateUserName("cliffhuxtable1986"))
.build();
client.execute(updateOp);
You may notice that a fetch option was added to our UpdateValue
operation: FetchValue.Option.DELETED_VCLOCK
was set to true
.
Remember from the section above that you should always read an object
before modifying and writing it, even if the object has been deleted.
Setting this option to true
ensures that the causal context is fetched
from Riak if the object has been deleted. We recommend always setting
this option to true
when constructing UpdateValue
operations.
Clobber Updates
If you’d like to update an object by simply replacing it with an
entirely new value of the same type (unlike in the section above, where
only one property of the object was updated), the Java client provides
you with a “clobber” update that you can use to replace the existing
object with a new object of the same type rather than changing one or
more properties of the object. Imagine that there is a User
object
stored in the bucket users
in the key cliffhuxtable1986
, as in the
example above, and we simply want to replace the object with a brand new
object:
Location location = new Location(new Namespace("users"), "cliffhuxtable1986");
User brandNewUser = new User(/* new user info */);
UpdateValue updateOp = new UpdateValue.Builder(Location)
// As before, we set this option to true
.withFetchOption(FetchValue.Option.DELETED_VCLOCK, true)
.withUpdate(Update.clobberUpdate(brandNewUser))
.build();
client.execute(updateOp);
No-operation Updates in Java
The Java client also enables you to construct no-operation updates that don’t actually modify the object and simply write the original value back to Riak. What is the use of that, given that it isn’t changing the value of the object at all? No-operation updates can be useful because they can help Riak resolve sibling conflicts. If you have an object—or many objects, for that matter—with siblings, a no-operation update will fetch the object and its causal context and write the object back to Riak with the same, fetched context. This has the effect of telling Riak that you deem this value to be most current. Riak can then use this information in internal sibling resolution operations.
Below is an example:
Location loc = new Location(...);
UpdateValue updateOp = new UpdateValue.Builder(loc)
.withUpdate(Update.noopUpdate())
.build();
client.execute(updateOp);
The example above would update the object without fetching it. You
could, however, use a no-operation update to read an object as well if
you set return_body
to true
in your request:
// Using the Location object "loc" from above:
UpdateValue updateOp = new UpdateValue.Builder(loc)
.withFetchOption(Option.RETURN_BODY, true)
.withUpdate(Update.noopUpdate())
.build();
UpdateValue.Response response = client.execute(updateOp);
RiakObject object = response.getValue(RiakObject.class);
// Or to continue the User example from above:
User user = response.getValue(User.class);
In general, you should use no-operation updates only on keys that you suspect may have accumulated siblings or on keys that are frequently updated (and thus bear the possibility of accumulating siblings). Otherwise, you’re better off performing normal reads.