Transactions
See Txn Request API and Txn Response API for the complete gRPC API documentation for retrieving records from Armada.
An Armada transaction is an atomic if/then/else construct over the key-value store. It provides a primitive grouping of requests whose execution is guarded, supporting the protection of data from concurrent modification.
Transactions consist of two parts - RequestOp operations and Compare
predicates. Conditional execution of transactions is
also supported.
To gain a better understanding of how to use the transaction API, see examples.
Retrieving or Modifying Data With Transactions
Transactions can be invoked by sending TxnRequest message via the
Txn remote procedure call, regatta.v1.KV/Txn, which returns TxnResponse message.
TxnRequest consists of Compare predicates, RequestOp operations to be executed
depending on the evaluation of the predicates, and the name of a table in which the transaction
will be executed.
TxnResponse consists of a ResponseHeader, ResponseOp responses, and a succeeded
field denoting whether the Compare predicates evaluated to true
(i.e. TxnResponse.succeeded == true) or not.
RequestOp messages are the basic building blocks of transactions.
These are the operations used to retrieve data from the data store or to
modify it. A RequestOp is one of Range, Put, or DeleteRange messages.
They can be guarded with predicates, as described in
the next section.
More detailed description and their features of the individual operations can be
found in the API documentation.
RequestOp messages are used in the success and failure repeated fields in
the Txn messages. This is the "execution body" of the if/then/else construct of
conditional execution described below. If a conditional
execution of transactions is not desired, supplement the RequestOp operations
only in the success field and leave the rest empty.
ResponseOp messages are the results of the operations in a given transaction.
A ResponseOp is one of Range, Put, or DeleteRange messages, depending on the
type of the corresponding RequestOp operation provided in the transaction.
An n-th ResponseOp message maps to an n-th RequestOp message in the transaction.
See the API documentation for more details.
Conditional Execution
Operations in transactions can be executed conditionally, after supplying a list of
predicates representing a logical conjunction of terms to be evaluated on the data
in Armada. This is the repeated compare field in the Txn protobuf message.
Depending on the result of the conjunction of the terms, either success
or failure operations are executed. If all of the compare terms evaluate
to true, then the success operations are executed. Otherwise, failure
operations are executed.
The predicates and the operations themselves form a single, non-divisible transaction.
This is the current Protobuf definition of the Compare message
(see the API documentation of Compare),
representing a single term in a given conjunction:
message Compare {
enum CompareResult {
EQUAL = 0;
GREATER = 1;
LESS = 2;
NOT_EQUAL = 3;
}
enum CompareTarget {
VALUE = 0;
}
// result is logical comparison operation for this comparison.
CompareResult result = 1;
// target is the key-value field to inspect for the comparison.
CompareTarget target = 2;
// key is the subject key for the comparison operation.
bytes key = 3;
oneof target_union {
// value is the value of the given key, in bytes.
bytes value = 4;
}
// range_end compares the given target to all keys in the range [key, range_end).
// See RangeRequest for more details on key ranges.
bytes range_end = 64;
}
CompareResult- logical operation to be performed on theCompareTarget. It must be one ofEQUAL,GREATER,LESS, orNOT_EQUAL. Testing for existence of a given key is described in Testing Existence of Key and testing for existence of a key within range is described in Testing Existence of Key Within Range.CompareTarget- domain on which theCompareResultis performed. OnlyVALUEis currently supported.
Testing Existence of Key
A predicate testing for the existence of a given key can also be created.
To do so, supply only the key in the Compare message. An example of a such
predicate can be found here.
Testing Existence of Key Within Range
To test the existence of some keys within a given range, supply only the
key and range_end in the Compare message. An example of a such predicate
can be found here.
Note that the predicate evaluates to false if and only if no key exists
in the provided range. Also, key and range_end form a right-open interval [key, range_end).
Examples
Transactions are executed via the regatta.v1.KV/Txn remote procedure call.
Transaction With No Predicates
Suppose we wish to atomically insert multiple records into table armada-test,
and list them back. We could achieve this by defining multiple PUT operations,
one for each record, and a RANGE operation, listing the inserted records,
all in the success branch.
grpcurl -insecure "-d={
\"table\": \"$(echo -n "armada-test" | base64)\",
\"success\": [{
\"request_put\": {
\"key\": \"$(echo -n "brba:walter" | base64)\",
\"value\": \"$(echo -n "white" | base64)\"
}
}, {
\"request_put\": {
\"key\": \"$(echo -n "brba:jessie" | base64)\",
\"value\": \"$(echo -n "pinkman" | base64)\"
}
}, {
\"request_put\": {
\"key\": \"$(echo -n "brba:hank" | base64)\",
\"value\": \"$(echo -n "schrader" | base64)\"
}
}, {
\"request_range\": {
\"key\": \"$(echo -n "brba:" | base64)\",
\"range_end\": \"$(echo -n "brbb:" | base64)\",
\"count_only\": \"true\"
}
}]
}" localhost:8443 regatta.v1.KV/Txn
This would be the expected response:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"revision": "8",
"raftTerm": "2",
"raftLeaderId": "1"
},
"succeeded": true,
"responses": [
{
"responsePut": {
}
},
{
"responsePut": {
}
},
{
"responsePut": {
}
},
{
"responseRange": {
"count": "3"
}
}
]
}
Transaction With Predicates
The following transaction checks whether there's a key-value pair
john:doe in table armada-test. If such a key-value pair
exists, a new record jane:doe is upserted in a compare-swap fashion,
as defined in the success branch. We also wish to retrieve the previous
key-value of the newly upserted record, as stated in success[0].request_put.prev_kv = true.
Mind the upper case EQUAL and VALUE special values for compare[0].result and
compare[0].target, respectively. The two records are then listed.
grpcurl -insecure "-d={
\"table\": \"$(echo -n "armada-test" | base64)\",
\"compare\": [{
\"result\": \"EQUAL\",
\"target\": \"VALUE\",
\"key\": \"$(echo -n "john" | base64)\",
\"value\": \"$(echo -n "doe" | base64)\"
}],
\"success\": [{
\"request_put\": {
\"key\": \"$(echo -n "jane" | base64)\",
\"value\": \"$(echo -n "doe" | base64)\",
\"prev_kv\": \"true\"
}
}, {
\"request_range\": {
\"key\": \"$(echo -n "jane" | base64)\"
}
}, {
\"request_range\": {
\"key\": \"$(echo -n "john" | base64)\"
}
}]
}" localhost:8443 regatta.v1.KV/Txn
This would be one of the possible responses if the record john:doe existed when the transaction was issued:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"revision": "12",
"raftTerm": "2",
"raftLeaderId": "1"
},
"succeeded": true,
"responses": [
{
"responsePut": {
}
},
{
"responseRange": {
"kvs": [
{
"key": "amFuZQ==",
"value": "ZG9l"
}
],
"count": "1"
}
},
{
"responseRange": {
"kvs": [
{
"key": "am9obg==",
"value": "ZG9l"
}
],
"count": "1"
}
}
]
}
Response if the record john:doe did not exist:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"revision": "11",
"raftTerm": "2",
"raftLeaderId": "1"
}
}
Additional operations can be provided in the failure branch,
which will execute when any of the predicates in compare evaluate
to false. Let's extend the previous example by inserting a different
record if the record john:doe is not found.
grpcurl -insecure "-d={
\"table\": \"$(echo -n "armada-test" | base64)\",
\"compare\": [{
\"result\": \"EQUAL\",
\"target\": \"VALUE\",
\"key\": \"$(echo -n "john" | base64)\",
\"value\": \"$(echo -n "doe" | base64)\"
}],
\"success\": [{
\"request_put\": {
\"key\": \"$(echo -n "jane" | base64)\",
\"value\": \"$(echo -n "doe" | base64)\",
\"prev_kv\": \"true\"
}
}, {
\"request_range\": {
\"key\": \"$(echo -n "jane" | base64)\"
}
}, {
\"request_range\": {
\"key\": \"$(echo -n "john" | base64)\"
}
}],
\"failure\": [{
\"request_put\": {
\"key\": \"$(echo -n "foo" | base64)\",
\"value\": \"$(echo -n "bar" | base64)\"
}
}]
}" localhost:8443 regatta.v1.KV/Txn
Before executing this transaction, delete the john:doe
record in the database to enforce execution of the failure branch.
This would then be the expected response:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"revision": "14",
"raftTerm": "2",
"raftLeaderId": "1"
},
"responses": [
{
"responsePut": {
}
}
]
}
Predicate Testing Existence of Key
The following predicate evaluates to true if a key-value pair with the key
john exists. If so, the success operations are then executed.
grpcurl -insecure "-d={
\"table\": \"$(echo -n "armada-test" | base64)\",
\"compare\": [{
\"key\": \"$(echo -n "john" | base64)\"
}],
\"success\": [{
\"request_put\": {
\"key\": \"$(echo -n "jane" | base64)\",
\"value\": \"$(echo -n "doe" | base64)\"
}
}]
}" localhost:8443 regatta.v1.KV/Txn
Suppose a key-value pair with the key john exists,
this would be the expected response:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"revision": "17",
"raftTerm": "2",
"raftLeaderId": "1"
},
"succeeded": true,
"responses": [
{
"responsePut": {
}
}
]
}
Otherwise, such response would be returned:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"raftTerm": "2",
"raftLeaderId": "1"
}
}
Predicate Testing Existence of Key Within Range
To test if there is any key-value pair between the keys jack
and john, excluding the key-value pair with the key john,
we supply jack and john as the key and range_end in
the predicate. If such pair exists, a count of such key-value
pairs is retrieved.
grpcurl -insecure "-d={
\"table\": \"$(echo -n "armada-test" | base64)\",
\"compare\": [{
\"key\": \"$(echo -n "jack" | base64)\",
\"range_end\": \"$(echo -n "john" | base64)\"
}],
\"success\": [{
\"request_range\": {
\"key\": \"$(echo -n "jack" | base64)\",
\"range_end\": \"$(echo -n "john" | base64)\",
\"count_only\": "true"
}
}]
}" localhost:8443 regatta.v1.KV/Txn
Suppose records with the keys alex, jack, jim, john, and pete exist.
This would be the expected response:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"raftTerm": "2",
"raftLeaderId": "1"
},
"succeeded": true,
"responses": [
{
"responseRange": {
"count": "2"
}
}
]
}
Note that
keyandrange_endform a right-open interval ([key, range_end)), hence the response does not contain the pair withjohnas a key.
If only keys alex, john, and pete exist, such response would be returned:
{
"header": {
"shardId": "10001",
"replicaId": "1",
"raftTerm": "2",
"raftLeaderId": "1"
}
}