tansu icon indicating copy to clipboard operation
tansu copied to clipboard

Tansu distributed key/value and lock store

Tansu

Build Status

Tansu is a distributed key value store designed to maintain configuration and other data that must be highly available. It uses the Raft Consensus algorithm for leadership election and distribution of state amongst its members. Node discovery is via mDNS and will automatically form a mesh of nodes sharing the same environment.

Features

Key Value Store

Tansu has a REST interface to set, get or delete the value represented by a key. It also provides a HTTP Server Sent Event Stream of changes to the store.

Check And Set

Tansu provides REST interface for simple Check And Set (CAS) operations.

Locks

Tansu provides test and set operations that can be used to operate locks through a simple REST based HTTP Server Sent Event Stream interface.

Quick Start

To start a 5 node Tansu cluster using Docker:

for i in {1..5}; do
    docker run \
        --name tansu-$(printf %03d $i) \
        -d shortishly/tansu;
done

The following examples use a random Tansu node:

RANDOM_IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} tansu-$(printf %03d $[1 + $[RANDOM % 5]]))

You can use the same ${RANDOM_IP} for each example, or pick a new one each time. Tansu will automatically proxy any requests that must be handled to the leader if necessary (typically, locks, CAS and writes are handled by the leader) or can be handled by a follower (reads are handled directly by followers).

Key Value Store

Stream changes to any key below "hello":

curl -i -s "http://${RANDOM_IP}/api/keys/hello?stream=true&children=true"

Note that you can create streams for keys that do not currently exist in the store. Once a value has been assigned to the key the stream will issue change notifications. You can also listen for changes to any key contained under the sub hierarchy by adding children=true to the query.

Set

In another shell assign the value "world" to the key "hello":

curl -X PUT -i -s http://${RANDOM_IP}/api/keys/hello -d value=world

The stream will now contain a create notification:

id: 1
event: create
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"text/plain","created":1,"parent":"/","updated":1}},"value":"world"}

Or a key that is below "hello":

curl -X PUT -i -s http://${RANDOM_IP}/api/keys/hello/joe -d value=mike

The stream will now contain a create notification:

id: 2
event: create
data: {"category":"user","key":"/hello/joe","metadata":{"tansu":{"content_type":"text/plain","created":2,"parent":"/hello","updated":2}},"value":"mike"}

Or with a content type:

curl -X PUT -H "Content-Type: application/json" -i http://${RANDOM_IP}/api/keys/hello --data-binary '{"stuff": true}'

With an update in the stream:

id: 3
event: set
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"application/json","created":1,"parent":"/","updated":3}},"previous":"world","value":{"stuff":true}}

Get

The current value of "hello":

curl -i -s http://${RANDOM_IP}/api/keys/hello
{"stuff": true}

Delete

Ask a random member of the cluster to delete the key "hello":

curl -i -X DELETE http://${RANDOM_IP}/api/keys/hello

The stream now contains a delete notification:

id: 5
event: delete
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"application/json","created":1,"parent":"/","updated":5}},"value":{"stuff":true}}

TTL

A value can also be given a time to live by also supplying a TTL header:

curl -X PUT -H "Content-Type: application/json" -H "ttl: 10" -i http://${RANDOM_IP}/api/keys/hello --data-binary '{"ephemeral": true}'

The event stream will contain details of the create together with a TTL attribute:

id: 6
event: create
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"application/json","created":6,"parent":"/","ttl":10,"updated":6}},"value":{"ephemeral":true}}

Ten seconds later when the key is removed:

id: 7
event: delete
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"application/json","created":6,"parent":"/","ttl":0,"updated":7}},"value":{"ephemeral":true}}

Test and Set

Set the value of /hello to be jack only if that key does not already exist:

curl -X PUT -i http://${RANDOM_IP}/api/keys/hello?prevExist=false -d value=jack

The stream identifies test and set changes with type=cas:

id: 8
event: set
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"text/plain","created":8,"parent":"/","type":"cas","updated":8}},"value":"jack"}

Set the value of /hello to be quentin only if its current value is jack:

curl -X PUT -i http://${RANDOM_IP}/api/keys/hello?prevValue=jack -d value=quentin
id: 9
event: set
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"text/plain","created":8,"parent":"/","type":"cas","updated":9}},"previous":"jack","value":"quentin"}

Delete the value of /hello only if its current value is quentin:

curl -X DELETE -i http://${RANDOM_IP}/api/keys/hello?prevValue=quentin
id: 10
event: delete
data: {"category":"user","key":"/hello","metadata":{"tansu":{"content_type":"text/plain","created":8,"parent":"/","updated":10}},"value":"quentin"}

Locks

Locks are obtained by issuing a HTTP GET on /api/locks/ followed by the name of the lock. The response is a Server Sent Event stream that will indicate the status the lock to the caller. The lock holder will retain the lock until the connection is dropped (either by the client or sever). The free lock is then automatically granted to a waiting connection.

In several different shells simultaneously request a lock on "abc":

curl -i -s http://${RANDOM_IP}/api/locks/abc
curl -i -s http://${RANDOM_IP}/api/locks/abc
curl -i -s http://${RANDOM_IP}/api/locks/abc

One shell is granted the lock, with the remaining shells waiting their turn. Drop the lock by hitting ^C on the holder, the lock is then allocated to another waiting shell.

Leadership Election

Tansu provides cluster information via the /api/info resource as follows, picking a random node:

curl -s http://${RANDOM_IP}/api/info|python -m json.tool

Each node may be in follower or candidate state, with only one node in the leader role:

{
    "applications": {
        "any": "rolling",
        "asn1": "4.0.2",
        "cowboy": "2.0.0-pre.2",
        "cowlib": "1.3.0",
        "crown": "0.0.1",
        "crypto": "3.6.3",
        "envy": "0.0.1",
        "gproc": "git",
        "gun": "1.0.0-pre.1",
        "inets": "6.2.2",
        "jsx": "2.8.0",
        "kernel": "4.2",
        "mdns": "0.4.1",
        "mnesia": "4.13.4",
        "public_key": "1.1.1",
        "ranch": "1.1.0",
        "recon": "2.2.1",
        "rfc4122": "0.0.3",
        "sasl": "2.7",
        "shelly": "0.1.0",
        "ssh": "4.2.2",
        "ssl": "7.3.1",
        "stdlib": "2.8",
        "tansu": "0.13.0"
    },
    "consensus": {
        "cluster": "fadbf747-f700-4d87-b986-73c2a1de18e4",
        "commit_index": 13668,
        "connections": {
            "1c64ca8a-303b-43f7-914f-09e3b680f9ed": {
                "host": "172.17.0.2",
                "port": 80
            },
            "41fc20ae-70be-4e9d-a40d-a8164b165283": {
                "host": "172.17.0.7",
                "port": 80
            },
            "6343a50d-acc6-4f62-866d-b5a7dcde5d04": {
                "host": "172.17.0.5",
                "port": 80
            },
            "e8d25d82-5f07-4598-bb0c-6074793ee111": {
                "host": "172.17.0.3",
                "port": 80
            },
            "e9057f76-24b5-4f4f-b81c-96d555798ef4": {
                "host": "172.17.0.4",
                "port": 80
            }
        },
        "env": "dev",
        "id": "f327cc37-114f-4237-942b-972199e364a1",
        "last_applied": 13668,
        "leader": {
            "commit_index": 13668,
            "id": "e8d25d82-5f07-4598-bb0c-6074793ee111"
        },
        "role": "follower",
        "term": 877
    },
    "version": {
        "major": 0,
        "minor": 13,
        "patch": 0
    }
}

This section optionally uses jq to parse some of the JSON output from Tansu. To install use:

dnf install -y jq

The following script iterates over the members of the Tansu cluster outputting the role of each member:

for i in {1..5};
do
NAME=tansu-$(printf %03d $i)
IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} ${NAME})
ROLE=$(curl -m 1 -s http://${IP}/api/info|jq .consensus.role)
echo ${NAME} ${ROLE}
done

As an example:

tansu-001 "follower"
tansu-002 "leader"
tansu-003 "follower"
tansu-004 "follower"
tansu-005 "follower"

Pause the leader (replace tansu-002 with your leader):

docker pause tansu-002

Check that one of the other nodes has been established as the leader by repeating the curl of /api/info on the remaining nodes:

for i in {1..5};
do
NAME=tansu-$(printf %03d $i)
IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} ${NAME})
ROLE=$(curl -m 1 -s http://${IP}/api/info|jq .consensus.role)
echo ${NAME} ${ROLE}
done

As an example, tansu-002 is now paused:

tansu-001 "leader"
tansu-002
tansu-003 "follower"
tansu-004 "follower"
tansu-005 "follower"

Fire some updates into one of the remaining nodes (change tansu-003 to a running node):

IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} tansu-003)
for i in {0..100};
do
    curl -X PUT -s http://${IP}/api/keys/pqr -d value=$i;
done

Tansu will create a snapshot of its current state every so often, and uses this snapshot when members join (or rejoin) the cluster together with any log entries subsequent to that snapshot.

Unpause the diposed former leader:

docker unpause tansu-002

Check that tansu-002 has rejoined the cluster and is now in the follower role:

for i in {1..5};
do
NAME=tansu-$(printf %03d $i)
IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} ${NAME})
ROLE=$(curl -m 1 -s http://${IP}/api/info|jq .consensus.role)
echo ${NAME} ${ROLE};
done

The node tansu-002 is now in the follower role:

tansu-001 "leader"
tansu-002 "follower"
tansu-003 "follower"
tansu-004 "follower"
tansu-005 "follower"

Ask the unpaused node for the value of pqr:

IP=$(docker inspect --format={{.NetworkSettings.IPAddress}} tansu-002)
curl -i http://${IP}/api/keys/pqr

The node will have same value as the remainder of the cluster:

100

Configuration

Tansu uses the following configuration environment.

environment variable default
TANSU_BATCH_SIZE_APPEND_ENTRIES 32
TANSU_CAN_ADVERTISE true
TANSU_CAN_MESH true
TANSU_SNAPSHOT_DIRECTORY /snapshots
TANSU_DEBUG false
TANSU_SM tansu_sm_mnesia_kv
TANSU_ENDPOINT_SERVER /server
TANSU_ENDPOINT_API /api
TANSU_HTTP_PORT 80
TANSU_DB_SCHEMA ram
TANSU_ENVIRONMENT dev
TANSU_ACCEPTORS 100
TANSU_TIMEOUT_ELECTION_LOW 1500
TANSU_TIMEOUT_ELECTION_HIGH 3000
TANSU_TIMEOUT_LEADER_LOW 500
TANSU_TIMEOUT_LEADER_HIGH 1000
TANSU_TIMEOUT_KV_EXPIRY 1000
TANSU_TIMEOUT_KV_SNAPSHOT 1000 * 60
TANSU_TIMEOUT_MNESIA_WAIT_FOR_TABLES infinity
TANSU_TIMEOUT_SYNC_SEND_EVENT infinity
TANSU_TIMEOUT_STREAM_PING 5000
TANSU_MINIMUM_QUORUM 3
TANSU_MAXIMUM_SNAPSHOT 3
TANSU_CLUSTER_MEMBERS