Java, etcd and the alternative to jetcd - etcd-java

Published

When developing with etcd, a good quality driver is essential. Now etcd-java has been open-sourced and Java developers have a choice of driver. We take a look at the recent arrival.

At Compose, we go through a lot of drivers and etcd 3 has been one of the more interesting environments for driver development. It seems to be something to do with the variability of support for gRPC, etcd 3's underlying wire protocol, on different languages and platforms. So, we were pleased to see etcd-java turn up to bolster support on Java.

Why a new driver?

A bit of background first; the official Java driver for etcd, jetcd has become, over time, a robust driver but in jetcd's early days it wasn't proving as reliable as might be hoped. The developers at IBM wanted something more robust for their production work and set about creating their own driver, etcd-java.

The design brief was to work well with their day-to-day feature use of etcd; connecting, working with keys and values, watching and leasing. With this pragmatic approach in mind, we set about building some examples with it to see how well it did. You'll find the example code in the compose-ex/etcdexample.

Connecting with etcd-java

One feature we noticed right away was support for Compose and IBM Cloud Compose etcd deployments so we got right down to creating some code to connect. The foundation of etcd-java's hierarchy is KvStoreClient which encapsulates the connection to the database.

import com.ibm.etcd.client.*;  
import com.ibm.etcd.client.config.ComposeTrustManagerFactory;  
import com.ibm.etcd.client.kv.KvClient;

...

    KvStoreClient client = null;

We'll now try and create a connection to a deployment by the name of "threetcd". That deployment has two endpoints so we'll start building the client with that:

    client = EtcdClient.forEndpoints("hostportal1.threetcd.compose-3.composedb.com:18279,hostportal2.threetcd.compose-3.composedb.com:18279")

But there's more. The etcd deployment has a root user with a password SECRETS so we add that to the client settings:

             .withCredentials("root", "secret")

And next, we need it to be able to establish a TLS connection and verify it. Etcd-java has the ability to add in a TrustManager to handle that verification. More importantly, it offers a ComposeTrustManagerFactory() that makes Compose-specific TrustManagers. Here, for our example, we'll give it just the name of our deployment.

             .withTrustManager(new ComposeTrustManagerFactory("threetcd"))

With all the settings in place, we can now build the connection... and remember to catch the possible exceptions.

             .build();

And now we are connected. This isn't the only way to configure the connection though; we've baked in all our configuration into the code here which is great for examples, not so good for production. Before you set about writing your own code to load config from a file, you need to know there's also the ability to configure the etcd-java connection from a JSON file built in. That makes connecting as easy as:

    KvStoreClient client = EtcdClusterConfig.fromJsonFile(filePath).getClient();

With an appropriate JSON file. You can see examples of this in the documentation for the JSON files. You can have as little as just the endpoints listed in there or you can specify user, password, certificates and deployment name. Using this makes it super-simple to swap configurations in an application or even save them as secrets in your infrastructure.

Working with the client

The KvStoreClient in etcd-java is actually home to two sub-clients, specifically the KvClient for interacting with the key/value database and the LeaseClient for managing leases on those keys. Our first stop is the KvClient. Let's get an instance of it from the client:

    KvClient kvclient = client.getKvClient();

Now, etcd-java supports synchronous and asynchronous calling, so let's start with setting and reading a key with the synchronous API:

    kvclient.put(bs("hello"), bs("test")).sync();

    RangeResponse result = kvclient.get(bs("hello")).sync();

    dumpRangeResponse(result);

So many things are in this simple block of code. The put operation should be obvious; putting a value of test into the database with a key of hello. But what's those bs() functions wrapping the strings? Well, it's a convenience function because most of the etcd-java calls use ByteStrings, rather than Strings. Now, you could wrap your strings with ByteString.copyFromUtf8("mystring") but it'd only ruin the readability. A function like...

    public static ByteString bs(String str) {
        return ByteString.copyFromUtf8(str);
    }

makes your code more readable and if you add

import static com.ibm.etcd.client.KeyUtils.bs;  

at the start of your code you can use the version built into the library. Next up is the .sync() at the end of the put. This sets the client up to run the command as synchronous. It'll block and wait for completion. In this case, the put will execute and return.

To see the sync() make a visible difference, we should look at the next line:

   RangeResponse result = kvclient.get(bs("hello")).sync();

Here, we get the value of the key we just set. We'll get back a RangeResponse, an encapsulation of Key/Value pairs with a count. The thing is, with the sync() we'll block till we have the data, or throw an exception.

How do we read that RangeResponse? Here, we have a small function to iterate over it and print the contents.

    public static void dumpRangeResponse(RangeResponse rr) {
        for(int i=0;i<rr.getKvsCount();i++) {
            KeyValue kv=rr.getKvs(i);
            System.out.println(i+" : "+kv.getKey().toStringUtf8()+" : "+kv.getValue().toStringUtf8());
        }
    }

This also does the reverse of bs() in that it uses .toStringUtf8() to covert ByteStrings to printable strings.

Going async

Working with the API in with asynchronous calls is basically the same but for any call, it ends with a .async() and it returns a ListenableFuture. For this to work though, we'll need a pool of threads:

    ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));

This makes a pool of threads to handle async requests. Now let's do our get again, but as an async call:

    final ListenableFuture<RangeResponse> fresult = kvclient.get(bs("hello")).async();

Now we're getting a result in the future, and it's going to be a RangeResponse again. That's now off happening and we need to set something up to manage the response:

    fresult.addListener(new Runnable() {
        @Override
        public void run() {
            try {
                final RangeResponse rr=fresult.get();
                dumpRangeResponse(rr);
            } catch (InterruptedException e) {
                log.error("Interrupted", e);
            } catch (ExecutionException e) {
                log.error("Exception in task", e.getCause());                
            }
        }
    }, service);

We add a Listener to the future here and that, in turn takes a Runnable. We then override the Run method with our handling code, which in this case just grabs the RangeResponse and calls out dumpRangeResponse utility method.

So when the response arrives, we'll be ready for it. Of course, our example is a "straight line" execution path so if we let it carry on running, it'd exit before the response got back. And thats why, in the example, we have a Thread.sleep() for a second while we wait for the asynchronous operation complete. Actual applications will have plenty to do and not need to idle which those database queries are being worked on.

It'll depend on how you are architecting your application as to whether you want synchronous or asynchronous calls, but you have the choice here and the actual command builders are the same for either.

Watching and waiting

So what else can you do with the etcd-java API. Well, you can watch for changes.

    KvClient.Watch watch = kvclient.watch(KvClient.ALL_KEYS).executor(threadpool).start(observer);

This is asking the client to watch all keys. Of course we have to pick up those updates and thats done by use creating a StreamObserver for WatchUpdates:

    final StreamObserver<WatchUpdate> observer = new StreamObserver<WatchUpdate>() {

        @Override
        public void onNext(WatchUpdate value) {
            System.out.println("watch event: "+value);
        }
        @Override
        public void onError(Throwable t) {
            System.out.println("watch error: "+t);
        }
        @Override
        public void onCompleted() {
            System.out.println("watch completed");
        }

Now, when any key changes, we get a watch event. We can make this focus on particular prefixes too by passing a prefix rather than KvClient.ALL_KEYS and adding .asPrefix(). Later in our example, we close the watch for all keys and make it look for updates with to a prefix of "/hello/".

    watch.close();

    watch = kvclient.watch(bs("/hello/")).asPrefix().executor(threadpool).start(observer);

    for(int n=1;n<100;n++) {
        kvclient.put(bs("/hello/" + n), bs("test")).sync();
    }

Remember that events may be batched up when coming back; for example, when we do a delete in our example, the observer is called each of our 99 keys - our first key is silently deleted because it doesn't match the prefix.

Other features

The etcd-java has a number of other features we should touch on here. Transactions, a key feature of etcd3, are enabled here through a fluent builder that makes it easy to compile the transaction. A LeaseClient give access to etcd3's lease API and the ability to observe the states of leases. And there's the ability to batch requests simply by bundling them together into a single client.batch().....async() request.

One thing we did find when working with this library is that it is strongly recommended to run it with Java 8. Java 9 and 10 have not seen some of the underlying libraries ported to it that etcd-java needs and, trust us, it leads to some odd behavior. Stick to Java 8 for now.

Wrapping up

What's worth noting is what's not in etcd-java. If you are looking to access all the etcd server management APIs - cluster management, maintentance and authorization - you're not going to find those calls in etcd-java. The developers have focussed on the needs of a typical client that needs to interact with the key-value store rather than manage the key-value store. This focus makes etcd-java a good choice for most applications wanting to integrate with other applications. If you're using etcd 3 and Java, do check it out. You can find it in its repository on Github. IBM/etcd-java

Dj Walker-Morgan
Dj Walker-Morgan was Compose's resident Content Curator, and has been both a developer and writer since Apples came in II flavors and Commodores had Pets. Love this article? Head over to Dj Walker-Morgan’s author page to keep reading.

Conquer the Data Layer

Spend your time developing apps, not managing databases.