Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An RSVP-list demo on top of Riak 2.0 Datatypes

An RSVP-list demo on top of Riak 2.0 Datatypes

This is a purely in-browser demo of Riak 2.0's sets datatype. The application code uses React.js and jquery.

To get started, make a 5-node devrel cluster of the 2.0 preview using these instructions. After your cluster is built, make a bucket-type for sets:

cd dev/dev1
bin/riak-admin bucket-type create sets '{"props":{"datatype":"set","allow_mult":true}}'
bin/riak-admin bucket-type activate sets

Upload the html and JS files into the same bucket:

curl -XPUT -d @setsdemo.js -H "Content-Type: application/javascript" http://localhost:10018/buckets/setsdemo/keys/setsdemo.js
curl -XPUT -d @setsdemo.html -H "Content-Type: text/html" http://localhost:10018/buckets/setsdemo/keys/setsdemo.html

Now open two browser tabs to these two URLS:

http://localhost:10018/buckets/setsdemo/keys/setsdemo.html
http://localhost:10048/buckets/setsdemo/keys/setsdemo.html

You should see the app looking identical in both tabs. Enter a name into the field and press Enter or click the "RSVP" button. Within a half-second, the other tab should show the name you entered (there is an Ajax polling of the set's value happening every 500ms). Click the "Remove" button next to the name to delete it, then it should disappear from the other tab.

Before we do the next part, make sure several names are in the RSVP list. For the examples below we will use the names: Alice, Bob, and Cheryl.

Partitioning

Now, the cool part about Riak's sets is not manipulating the sets themselves, but their partition-tolerance. That is, if you update them on both sides of a network partition, they will resolve to values that make sense given the operations performed. This also applies to race-conditions, but humans are not fast enough to make both browser windows send concurrent updates within the time it takes to replicate a single update.

So, let's partition the cluster and update it on both sides. First attach directly to node 1 of the cluster:

# If you're not already in dev/dev1, cd into there
bin/riak attach-direct

Now you should see the Riak prompt with the node name listed (NOTE: Do not press Ctrl-C in this shell, use Ctrl-D to detach). Compile and load the demo module:

(dev1@127.0.0.1) 1> c("partdemo.erl").
{module, partdemo}
(dev1@127.0.0.1) 2> partdemo:part().
true

At this point, your cluster is split into two groups: {dev1, dev2} and {dev3, dev4, dev5}. You may start seeing warnings printed to the open console, this is completely normal. We pointed our browser tabs to dev1 and dev4, respectively, so updates to the RSVP list will go to opposite sides of the partition.

Let's remove Bob from the dev1 side, and add David to the dev4 side. At this point, the RSVP lists should be divergent, looking like so:

Alice Alice
--- Bob
Cheryl Cheryl
--- David

The resolved version of this set should be Alice, Cheryl and David. Now let's heal the cluster and make sure that is the case, again from the attached Riak shell.

(dev1@127.0.0.1) 3> partdemo:heal().

Within a very short moment you should see both RSVP lists come together to the same value.

-module(partdemo).
-compile(export_all).
part() ->
true = rpc:call('dev2@127.0.0.1', erlang, set_cookie, ['dev2@127.0.0.1', riak2]),
true = erlang:set_cookie(node(), riak2),
true = rpc:call('dev2@127.0.0.1', erlang, disconnect_node, ['dev3@127.0.0.1']),
true = rpc:call('dev2@127.0.0.1', erlang, disconnect_node, ['dev4@127.0.0.1']),
true = erlang:disconnect_node('dev3@127.0.0.1'),
true = erlang:disconnect_node('dev4@127.0.0.1'),
true = erlang:disconnect_node('dev5@127.0.0.1').
heal() ->
true = rpc:call('dev2@127.0.0.1', erlang, set_cookie, ['dev2@127.0.0.1', riak]),
true = erlang:set_cookie(node(), riak),
rpc:abcast(riak_core_node_watcher, broadcast).
<!DOCTYPE html>
<html lang="en">
<head>
<title>RSVP</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script src="http://fb.me/react-0.9.0.js"></script>
<script src="http://fb.me/JSXTransformer-0.9.0.js"></script>
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
<div id="content" class="container"></div>
<script type="text/jsx" src="setsdemo.js"></script>
</body>
</html>
/**
* @jsx React.DOM
*/
var set_url = "/types/sets/buckets/demo/datatypes/rsvps";
var reload_data = function(data) {
React.renderComponent(<RSVPApp context={data.context}
entries={data.value} />,
document.getElementById('content'));
};
var remove = function(name, context) {
$.ajax(set_url + "?returnbody=true",
{
type: "POST",
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ remove: name, context: context })
}).done(reload_data);
};
var add = function(name) {
$.ajax(set_url + "?returnbody=true",
{
type: "POST",
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ add: name })
}).done(reload_data);
};
var RSVPApp = React.createClass({
render: function(){
return (<div>
<h1>RSVP List</h1>
<RSVPForm />
<RSVPList context={this.props.context}
entries={this.props.entries} />
</div>);
}
});
var RSVPForm = React.createClass({
getInitialState: function(){
return {value: ''};
},
handleChange: function(event){
this.setState({value: event.target.value});
},
handleSubmit: function() {
add(this.state.value);
this.setState({value: ''});
return false;
},
componentDidMount: function(){
this.refs.input.getDOMNode().autocomplete = "off";
},
render: function(){
return (
<form method="POST" className="form-inline" role="form"
onSubmit={this.handleSubmit}>
<div className="form-group">
<label className="sr-only" for="name">Name</label>
<input type="text" name="name" ref="input"
className="form-control"
placeholder="Name"
value={this.state.value}
onChange={this.handleChange} />
</div>
<div className="form-group"><button type="submit" className="btn btn-primary">RSVP</button></div>
</form>
);
}
});
var RSVPList = React.createClass({
render: function(){
var context = this.props.context;
var items = this.props.entries.map(function(name){
return <RSVPItem key={name} context={context} name={name} />;
});
return (<div className="container"> {items} </div>);
}
});
var RSVPItem = React.createClass({
handleClick: function(event){
remove(this.props.name, this.props.context);
},
render: function(){
return (
<div className="row">
<div className="col-xs-2">{this.props.name}</div>
<div className="col-md-2 col-xs-2">
<button className="btn btn-danger btn-xs"
onClick={this.handleClick}>Remove</button>
</div>
</div>
);
}
});
var handle_not_found = function(){
console.log(arguments);
};
var update = function(){
$.getJSON(set_url + "?include_context=true").done(reload_data).
fail(handle_not_found);
};
reload_data({"value":[], "context":undefined});
window.setInterval(update, 500);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.