Skip to content

Commit

Permalink
feat(kv): add put_not_exists and compare_and_swap transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
caspiano committed Jan 13, 2020
1 parent 25a02e6 commit dd0c2a1
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 15 deletions.
4 changes: 2 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: etcd
version: 0.1.0
version: 0.2.0

authors:
- Caspian Baska <[email protected]>
Expand All @@ -14,5 +14,5 @@ development_dependencies:
github: crystal-ameba/ameba
version: ~> 0.10.1

crystal: 0.30.1
crystal: 0.32.0
license: MIT
36 changes: 31 additions & 5 deletions spec/kv_spec.cr
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
require "uuid"
require "./helper"

module Etcd
describe Kv do
pending "compare_and_swap" do
describe "compare_and_swap" do
it "puts if compare succeeds" do
client = Etcd.from_env
key = "#{TEST_PREFIX}/hello"
value0 = "world"
value1 = "friends"
client.kv.put(key, value0)
response = client.kv.compare_and_swap(key, value: value1, previous_value: value0)
pp! response
success = client.kv.compare_and_swap(key, value: value1, previous_value: value0)
success.should be_true
client.kv.get(key).should eq value1
end

it "fails if compare fails" do
Expand All @@ -19,8 +21,32 @@ module Etcd
value0 = "world"
value1 = "friends"
client.kv.put(key, value0)
response = client.kv.compare_and_swap(key, value: value1, previous_value: "ginger nut")
pp! response
success = client.kv.compare_and_swap(key, value: value1, previous_value: "ginger nut")
success.should be_false
client.kv.get(key).should eq value0
end
end

describe "put_not_exists" do
it "succeeds if no key present" do
client = Etcd.from_env
lease = client.lease.grant 5
key = "#{TEST_PREFIX}/#{UUID.random}"
value = "hello world"
success = client.kv.put_not_exists(key, value: value, lease: lease[:id])
success.should be_true
client.kv.get(key).should eq value
end

it "fails if a key is present" do
client = Etcd.from_env
key = "#{TEST_PREFIX}/#{UUID.random}"
value0 = "hello world"
value1 = "bye world"
client.kv.put(key, value: value0)
success = client.kv.put_not_exists(key, value: value1)
success.should be_false
client.kv.get(key).should eq value0
end
end

Expand Down
6 changes: 4 additions & 2 deletions src/etcd/api.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ class Etcd::Api
value.transform_values { |v| to_stringly(v) }
when NamedTuple
to_stringly(value.to_h)
when String, Bool
value
when String
value.as(String)
when Bool
value.as(Bool)
else
value.to_s
end
Expand Down
42 changes: 36 additions & 6 deletions src/etcd/kv.cr
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,40 @@ class Etcd::Kv
# Non-Standard Requests
##############################################################################

def compare_and_swap(key, value, previous_value)
key, value, previous_value = {key, value, previous_value}.map &->Base64.strict_encode(String)
# Sets a key if the key is not already present.
#
# Wrapper over the etcd transaction API.
def put_not_exists(key : String, value, lease : Int64 = 0_i64) : Bool
key = Base64.strict_encode(key)
value = Base64.strict_encode(value.to_s)
post_body = {
:compare => [{
:key => key,
:value => Base64.strict_encode("0"),
:target => "VERSION",
:result => "EQUAL",
}],
:success => [{
:request_put => {
:key => key,
:value => value,
:lease => lease,
:ignore_lease => false,
},
}],
}

response = client.api.post("/kv/txn", post_body)
Model::TxnResponse.from_json(response.body).succeeded
end

# Sets a `key` if the given `previous_value` matches the existing value for `key`
#
# Wrapper over the etcd transaction API.
def compare_and_swap(key, value, previous_value) : Bool
key = Base64.strict_encode(key)
value = Base64.strict_encode(value.to_s)
previous_value = Base64.strict_encode(previous_value.to_s)
post_body = {
:compare => [{
:key => key,
Expand All @@ -113,10 +145,8 @@ class Etcd::Kv
}],
}

puts post_body.to_json
response = client.api.post("/kv/txn", post_body.to_json)

response
response = client.api.post("/kv/txn", post_body)
Model::TxnResponse.from_json(response.body).succeeded
end

def get(key) : String?
Expand Down
6 changes: 6 additions & 0 deletions src/etcd/model/kv.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ module Etcd::Model
getter deleted : Int32?
getter prev_kvs : Array(Kv)?
end

class TxnResponse < Base
getter header : Header
getter succeeded : Bool = false
getter responses : Array(JSON::Any) = [] of JSON::Any
end
end

0 comments on commit dd0c2a1

Please sign in to comment.