EtcdNet
EtcdNet is a .NET client library to access etcd (protocol V2), which is a distributed, consistent key-value store for shared configuration and service discovery.
- Provides API for all key space operations
- Support authentication & client-certificate
- Support etcd cluster & failover
- Lightweight & zero dependency on other assembly
- Task-based Asynchronous Pattern (TAP) API
- Structured Exceptions
- .NET Framework minimal requirement : v4.5
Get Started
Installation
To install etcdnet, run the following command in the Package Manager Console of Visual Studio. Or you can search etcdnet
in NuGet
Install-Package etcdnet
Basic Usage
Instantiate EtcdClient
class, then make the call.
using EtcdNet;
var options = new EtcdClientOpitions() {
Urls = new string[] { "http://etcd0.em:2379" },
//...
};
EtcdClient etcdClient = new EtcdClient(options);
string value = await etcdClient.GetNodeValueAsync("/some-key");
//...
Here you can find detailed api doc for EtcdClient
class. More examples can be found below.
EtcdClientOpitions
EtcdClientOpitions
allows to customize the EtcdClient
.
EtcdClientOpitions options = new EtcdClientOpitions() {
Urls = new string[] { "https://server1", "https://server2", "https://server3" },
Username = "username",
Password = "password",
UseProxy = false,
IgnoreCertificateError = true,
X509Certificate = new X509Certificate2(@"client.p12"),
JsonDeserializer = new NewtonsoftJsonDeserializer(),
};
-
Urls
If you are running a etcd cluster, more then one urls here. -
Username
&Password
are required when etcd enables basic authentication -
UseProxy
controls if use system proxy -
IgnoreCertificateError
ignores untrusted server SSL certificates. This is useful if you are using a self-signed SSL cert. -
X509Certificate
is required when etcd enabled client certification. -
JsonDeserializer
allows you to choose a different JSON deserializer. EtcdNet aims to avoid dependency on other 3rd-party assembly. Hence it takes use of the built-inDataContractJsonSerializer
to deserialize JSON. This parameter allows you to use other JSON deserializer like Newtonsoft.Json or ServiceStack.Text.
class NewtonsoftJsonDeserializer : EtcdNet.IJsonDeserializer
{
public T Deserialize<T>(string json)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json);
}
}
Thread Safety
The implementation of EtcdClient
class is guaranteed to be thread-safe, which means the methods of the same instance can be called from different threads without synchronization.
Further, it is recommended to use only one EtcdClient
instance to talk to the same etcd cluster. System.Net.Http.HttpClient
class, which emits HTTP requests internally, uses its own connection pool, isolating its requests from requests executed by other HttpClient instances. Sharing the same EtcdClient
instance helps to utilize features like HTTP pipelining.
Exception Handling
Each of the error code defined by etcd is mapped to an individual exception class.
EtcdGenericException
├── EtcdCommonException
| ├─ KeyNotFound
| ├─ TestFailed
| ├─ NotFile
| ├─ NotDir
| ├─ NodeExist
| ├─ RootReadOnly
| └─ DirNotEmpty
├── EtcdPostFormException
| ├─ PrevValueRequired
| ├─ TTLNaN
| ├─ IndexNaN
| ├─ InvalidField
| └─ InvalidForm
├── EtcdRaftException
| ├─ RaftInternal
| └─ LeaderElect
└── EtcdException
├─ WatcherCleared
└─ EventIndexCleared
Hence you have the choice to handle a specific error, or a group errors.
try {
//...
}
catch (EtcdCommonException.KeyNotFound) {
// 100 error
}
catch (EtcdCommonException.NodeExist) {
// 105 error
}
catch (EtcdCommonException) {
// 100-199 errors
}
catch (EtcdGenericException) {
// all etcd errors
}
Some methods which accept ignoreKeyNotFoundException
parameter, allows you to ignore EtcdCommonException.KeyNotFound
exception to make the code simpler.
Examples
Update by key
await etcdClient.SetNodeAsync(key, "value to be set");
Get a key
try {
EtcdResponse resp = await etcdClient.GetNodeAsync(key);
}
catch(EtcdCommonException.KeyNotFound) {
// key does not exist
}
Get a key (ignore key-not-found error)
EtcdResponse resp = await etcdClient.GetNodeAsync(key, ignoreKeyNotFoundException: true);
Get value by key
string value = await etcdClient.GetNodeValueAsync(key, ignoreKeyNotFoundException: true);
Create a new key
try {
EtcdResponse resp = await etcdClient.CreateNodeAsync(key, value);
}
catch (EtcdCommonException.NodeExist) {
// node already exists
}
Create a in-order key
etcdClient.CreateInOrderNodeAsync(key, value, ttl: 3);
Delete a key
try {
EtcdResponse resp = await etcdClient.DeleteNodeAsync(key);
}
catch(EtcdCommonException.KeyNotFound) {
// key does not exist
}
Delete a key (ignore key-not-found error)
await etcdClient.DeleteNodeAsync(key, ignoreKeyNotFoundException: true);
List child keys in order
try {
EtcdResponse resp = await etcdClient.GetNodeAsync(key, recursive: true, sorted:true);
if (resp.Node.Nodes != null) {
foreach (var node in resp.Node.Nodes)
{
// child node
}
}
}
catch (EtcdCommonException.KeyNotFound) {
// key does not exist
}
Compare and Swap (by value)
string prevValue = ...;
try {
EtcdResponse resp = await etcdClient.CompareAndSwapNodeAsync(key, prevValue, newValue);
}
catch (EtcdCommonException.KeyNotFound) {
// key does not exist
}
catch (EtcdCommonException.TestFailed) {
// supplied prevValue does not match
}
Compare and Swap (by index)
long prevIndex = ...;
try {
EtcdResponse resp = await etcdClient.CompareAndSwapNodeAsync(key, prevIndex, newValue);
}
catch (EtcdCommonException.KeyNotFound) {
// key does not exist
}
catch (EtcdCommonException.TestFailed) {
// supplied prevIndex does not match
}
Compare and Delete (by value)
string prevValue = ...;
try {
EtcdResponse resp = await etcdClient.CompareAndDeleteNodeAsync(key, prevValue);
}
catch (EtcdCommonException.KeyNotFound) {
// key does not exist
}
catch (EtcdCommonException.TestFailed) {
// supplied prevValue does not match
}
Compare and Delete (by index)
long prevIndex = ...;
try {
EtcdResponse resp = await etcdClient.CompareAndDeleteNodeAsync(key, prevIndex);
}
catch (EtcdCommonException.KeyNotFound) {
// key does not exist
}
catch (EtcdCommonException.TestFailed) {
// supplied prevValue does not match
}
Keep key alive
async void KeepAlive()
{
string key = "/my/key";
string value = ...;
const int ttl = 20; // seconds
while (_running)
{
try
{
await _etcdClient.SetNodeAsync(key, value, ttl: ttl);
if (!_running) return;
await Task.Delay(ttl / 2 * 1000);
continue;
}
catch (EtcdGenericException ege)
{
// etcd returns an error code
}
catch (Exception ex)
{
// a generic error
}
if (!_running) return;
// something went wrong, delay 1 second and try again
await Task.Delay(1000);
}
}
Watch changes
async void WatchChanges()
{
string key = "/my/key";
long? waitIndex = null;
EtcdResponse resp;
while (_running)
{
try
{
// when waitIndex is null, get it from the ModifiedIndex
if( !waitIndex.HasValue )
{
resp = await _etcdClient.GetNodeAsync( key, recursive: true);
if( resp != null && resp.Node != null )
{
waitIndex = resp.Node.ModifiedIndex + 1;
// and also check the children
if( resp.Node.Nodes != null )
{
foreach( var child in resp.Node.Nodes )
{
if (child.ModifiedIndex >= waitIndex.Value)
waitIndex = child.ModifiedIndex + 1;
// child node
}
}
}
}
// watch the changes
resp = await _etcdClient.WatchNodeAsync(key, recursive: true, waitIndex: waitIndex);
if (resp != null && resp.Node != null)
{
waitIndex = resp.Node.ModifiedIndex + 1;
if (resp.Node.Key.StartsWith(key, StringComparison.InvariantCultureIgnoreCase))
{
switch(resp.Action.ToLowerInvariant())
{
case EtcdResponse.ACTION_DELETE:
break;
case EtcdResponse.ACTION_EXPIRE:
break;
case EtcdResponse.ACTION_COMPARE_AND_DELETE:
break;
case EtcdResponse.ACTION_SET:
break;
case EtcdResponse.ACTION_CREATE:
break;
case EtcdResponse.ACTION_COMPARE_AND_SWAP:
break;
default:
break;
}
}
}
continue;
}
catch(TaskCanceledException)
{
// time out, try again
}
catch(EtcdException ee)
{
// reset the waitIndex
waitIndex = null;
}
catch (EtcdGenericException ege)
{
// etcd returns an error
}
catch (Exception ex)
{
// generic error
}
if (!_running) return;
// something went wrong, delay 1 second and try again
await Task.Delay(1000);
}
}