From a6a5b5458d7a2b48747d1fa13dd57bc1862c4cd5 Mon Sep 17 00:00:00 2001 From: Steven Schlansker Date: Fri, 21 Mar 2014 15:19:18 -0700 Subject: [PATCH 1/2] Expose etcd index metadata https://coreos.com/docs/distributed-configuration/etcd-api/#response-headers --- .../java/com/justinsb/etcd/EtcdClient.java | 30 +++++++++++++- .../java/com/justinsb/etcd/EtcdResult.java | 5 +++ .../java/com/justinsb/etcd/SmokeTest.java | 40 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/justinsb/etcd/EtcdClient.java b/src/main/java/com/justinsb/etcd/EtcdClient.java index babc363..36bcebf 100644 --- a/src/main/java/com/justinsb/etcd/EtcdClient.java +++ b/src/main/java/com/justinsb/etcd/EtcdClient.java @@ -246,6 +246,10 @@ private EtcdResult jsonToEtcdResult(JsonResponse response, int... expectedErrorC } EtcdResult result = parseEtcdResult(response.json); + result.etcdIndex = response.etcdIndex; + result.raftIndex = response.raftIndex; + result.raftTerm = response.raftTerm; + if (result.isError()) { if (!contains(expectedErrorCodes, result.errorCode)) { throw new EtcdClientException(result.message, result); @@ -343,10 +347,14 @@ public ListenableFuture apply(HttpResponse httpResponse) throws Ex static class JsonResponse { final String json; final int httpStatusCode; + final long etcdIndex, raftIndex, raftTerm; - public JsonResponse(String json, int statusCode) { + public JsonResponse(String json, int statusCode, long etcdIndex, long raftIndex, long raftTerm) { this.json = json; this.httpStatusCode = statusCode; + this.etcdIndex = etcdIndex; + this.raftIndex = raftIndex; + this.raftTerm = raftTerm; } } @@ -375,12 +383,30 @@ protected JsonResponse extractJsonResponse(HttpResponse httpResponse, int[] expe } } - return new JsonResponse(json, statusCode); + final long etcdIndex = parseLongHeader(httpResponse.getFirstHeader("X-Etcd-Index")); + final long raftIndex = parseLongHeader(httpResponse.getFirstHeader("X-Raft-Index")); + final long raftTerm = parseLongHeader(httpResponse.getFirstHeader("X-Raft-Term")); + + return new JsonResponse(json, statusCode, etcdIndex, raftIndex, raftTerm); } finally { close(httpResponse); } } + private static long parseLongHeader(Header header) + { + return parseLongHeader(header, Long.MIN_VALUE); + } + + private static long parseLongHeader(Header header, long dfl) + { + if (header == null) { + return dfl; + } else { + return Long.parseLong(header.getValue()); + } + } + private URI buildKeyUri(String prefix, String key, String suffix) { StringBuilder sb = new StringBuilder(); sb.append(prefix); diff --git a/src/main/java/com/justinsb/etcd/EtcdResult.java b/src/main/java/com/justinsb/etcd/EtcdResult.java index 5fd7364..4db41cb 100644 --- a/src/main/java/com/justinsb/etcd/EtcdResult.java +++ b/src/main/java/com/justinsb/etcd/EtcdResult.java @@ -12,6 +12,11 @@ public class EtcdResult { public String cause; public int errorIndex; + // Server metadata + public long etcdIndex; + public long raftIndex; + public long raftTerm; + public boolean isError() { return errorCode != null; } diff --git a/src/test/java/com/justinsb/etcd/SmokeTest.java b/src/test/java/com/justinsb/etcd/SmokeTest.java index ee42189..4de2eb3 100644 --- a/src/test/java/com/justinsb/etcd/SmokeTest.java +++ b/src/test/java/com/justinsb/etcd/SmokeTest.java @@ -230,4 +230,44 @@ public void testGetVersion() throws Exception { Assert.assertTrue(version.startsWith("etcd 0.")); } + @Test + public void testResponseMetadata() throws Exception { + String key = prefix + "/message"; + + long etcdIndex, raftIndex, raftTerm; + + EtcdResult result; + + result = this.client.set(key, "hello"); + + etcdIndex = result.etcdIndex; + raftIndex = result.raftIndex; + raftTerm = result.raftTerm; + + Assert.assertTrue(etcdIndex > 0); + Assert.assertTrue(raftIndex > 0); + Assert.assertTrue(raftTerm >= 0); + + result = this.client.get(key); + + Assert.assertEquals(etcdIndex, result.etcdIndex); + Assert.assertTrue(raftIndex <= result.raftIndex); + Assert.assertTrue(raftTerm <= result.raftTerm); + raftIndex = result.raftIndex; + raftTerm = result.raftTerm; + + result = this.client.set(key, "world"); + + Assert.assertTrue(etcdIndex < result.etcdIndex); + Assert.assertTrue(raftIndex < result.raftIndex); + Assert.assertTrue(raftTerm <= result.raftTerm); + etcdIndex = result.etcdIndex; + raftIndex = result.raftIndex; + raftTerm = result.raftTerm; + + result = this.client.get(key); + Assert.assertEquals(etcdIndex, result.etcdIndex); + Assert.assertTrue(raftIndex <= result.raftIndex); + Assert.assertTrue(raftTerm <= result.raftTerm); + } } From 8fde1a19393f1409544aeaac55feb93a88e09493 Mon Sep 17 00:00:00 2001 From: Steven Schlansker Date: Fri, 21 Mar 2014 15:20:41 -0700 Subject: [PATCH 2/2] Remove hard tabs, clean up incorrect indentation --- .../java/com/justinsb/etcd/EtcdClient.java | 31 +- .../justinsb/etcd/EtcdClientException.java | 4 +- src/main/java/com/justinsb/etcd/EtcdNode.java | 28 +- .../java/com/justinsb/etcd/EtcdResult.java | 40 +- .../java/com/justinsb/etcd/SmokeTest.java | 423 +++++++++--------- 5 files changed, 264 insertions(+), 262 deletions(-) diff --git a/src/main/java/com/justinsb/etcd/EtcdClient.java b/src/main/java/com/justinsb/etcd/EtcdClient.java index 36bcebf..231108c 100644 --- a/src/main/java/com/justinsb/etcd/EtcdClient.java +++ b/src/main/java/com/justinsb/etcd/EtcdClient.java @@ -114,16 +114,16 @@ public EtcdResult createDirectory(String key) throws EtcdClientException { data.add(new BasicNameValuePair("dir", "true")); return set0(key, data, new int[] { 200, 201 }); } - + /** * Lists a directory */ public List listDirectory(String key) throws EtcdClientException { - EtcdResult result = get(key + "/"); - if (result == null || result.node == null) { - return null; - } - return result.node.nodes; + EtcdResult result = get(key + "/"); + if (result == null || result.node == null) { + return null; + } + return result.node.nodes; } /** * Delete a directory @@ -156,13 +156,13 @@ public ListenableFuture watch(String key) throws EtcdClientException * Watches the given subtree */ public ListenableFuture watch(String key, Long index, boolean recursive) throws EtcdClientException { - String suffix = "?wait=true"; - if (index != null) { - suffix += "&waitIndex=" + index; - } - if (recursive) { - suffix += "&recursive=true"; - } + String suffix = "?wait=true"; + if (index != null) { + suffix += "&waitIndex=" + index; + } + if (recursive) { + suffix += "&recursive=true"; + } URI uri = buildKeyUri("v2/keys", key, suffix); HttpGet request = new HttpGet(uri); @@ -211,6 +211,7 @@ protected ListenableFuture asyncExecute(HttpUriRequest request, int[ throws EtcdClientException { ListenableFuture json = asyncExecuteJson(request, expectedHttpStatusCodes); return Futures.transform(json, new AsyncFunction() { + @Override public ListenableFuture apply(JsonResponse json) throws Exception { EtcdResult result = jsonToEtcdResult(json, expectedErrorCodes); return Futures.immediateFuture(result); @@ -334,6 +335,7 @@ protected ListenableFuture asyncExecuteJson(HttpUriRequest request ListenableFuture response = asyncExecuteHttp(request); return Futures.transform(response, new AsyncFunction() { + @Override public ListenableFuture apply(HttpResponse httpResponse) throws Exception { JsonResponse json = extractJsonResponse(httpResponse, expectedHttpStatusCodes); return Futures.immediateFuture(json); @@ -427,14 +429,17 @@ protected ListenableFuture asyncExecuteHttp(HttpUriRequest request final SettableFuture future = SettableFuture.create(); httpClient.execute(request, new FutureCallback() { + @Override public void completed(HttpResponse result) { future.set(result); } + @Override public void failed(Exception ex) { future.setException(ex); } + @Override public void cancelled() { future.setException(new InterruptedException()); } diff --git a/src/main/java/com/justinsb/etcd/EtcdClientException.java b/src/main/java/com/justinsb/etcd/EtcdClientException.java index 72ebcdb..7f27b66 100644 --- a/src/main/java/com/justinsb/etcd/EtcdClientException.java +++ b/src/main/java/com/justinsb/etcd/EtcdClientException.java @@ -26,9 +26,9 @@ public EtcdClientException(String message, EtcdResult result) { this.httpStatusCode = null; this.result = result; } - + public int getHttpStatusCode() { - return httpStatusCode; + return httpStatusCode; } public boolean isHttpError(int httpStatusCode) { diff --git a/src/main/java/com/justinsb/etcd/EtcdNode.java b/src/main/java/com/justinsb/etcd/EtcdNode.java index 81215e6..af3e318 100644 --- a/src/main/java/com/justinsb/etcd/EtcdNode.java +++ b/src/main/java/com/justinsb/etcd/EtcdNode.java @@ -3,21 +3,21 @@ import java.util.List; public class EtcdNode { - public String key; - public long createdIndex; - public long modifiedIndex; - public String value; + public String key; + public long createdIndex; + public long modifiedIndex; + public String value; - // For TTL keys - public String expiration; - public Integer ttl; + // For TTL keys + public String expiration; + public Integer ttl; - // For listings - public boolean dir; - public List nodes; + // For listings + public boolean dir; + public List nodes; - @Override - public String toString() { - return EtcdClient.format(this); - } + @Override + public String toString() { + return EtcdClient.format(this); + } } diff --git a/src/main/java/com/justinsb/etcd/EtcdResult.java b/src/main/java/com/justinsb/etcd/EtcdResult.java index 4db41cb..6d3d838 100644 --- a/src/main/java/com/justinsb/etcd/EtcdResult.java +++ b/src/main/java/com/justinsb/etcd/EtcdResult.java @@ -1,28 +1,28 @@ package com.justinsb.etcd; public class EtcdResult { - // General values - public String action; - public EtcdNode node; - public EtcdNode prevNode; + // General values + public String action; + public EtcdNode node; + public EtcdNode prevNode; - // For errors - public Integer errorCode; - public String message; - public String cause; - public int errorIndex; + // For errors + public Integer errorCode; + public String message; + public String cause; + public int errorIndex; - // Server metadata - public long etcdIndex; - public long raftIndex; - public long raftTerm; + // Server metadata + public long etcdIndex; + public long raftIndex; + public long raftTerm; - public boolean isError() { - return errorCode != null; - } + public boolean isError() { + return errorCode != null; + } - @Override - public String toString() { - return EtcdClient.format(this); - } + @Override + public String toString() { + return EtcdClient.format(this); + } } diff --git a/src/test/java/com/justinsb/etcd/SmokeTest.java b/src/test/java/com/justinsb/etcd/SmokeTest.java index 4de2eb3..8585bf3 100644 --- a/src/test/java/com/justinsb/etcd/SmokeTest.java +++ b/src/test/java/com/justinsb/etcd/SmokeTest.java @@ -5,230 +5,227 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import com.google.common.util.concurrent.ListenableFuture; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import com.google.common.util.concurrent.ListenableFuture; -import com.justinsb.etcd.EtcdClient; -import com.justinsb.etcd.EtcdClientException; -import com.justinsb.etcd.EtcdResult; - public class SmokeTest { - String prefix; - EtcdClient client; + String prefix; + EtcdClient client; + + @Before + public void initialize() { + this.prefix = "/unittest-" + UUID.randomUUID().toString(); + this.client = new EtcdClient(URI.create("http://127.0.0.1:4001/")); + } + + @Test + public void setAndGet() throws Exception { + String key = prefix + "/message"; + + EtcdResult result; + + result = this.client.set(key, "hello"); + Assert.assertEquals("set", result.action); + Assert.assertEquals("hello", result.node.value); + Assert.assertNull(result.prevNode); + + result = this.client.get(key); + Assert.assertEquals("get", result.action); + Assert.assertEquals("hello", result.node.value); + Assert.assertNull(result.prevNode); + + result = this.client.set(key, "world"); + Assert.assertEquals("set", result.action); + Assert.assertEquals("world", result.node.value); + Assert.assertNotNull(result.prevNode); + Assert.assertEquals("hello", result.prevNode.value); + + result = this.client.get(key); + Assert.assertEquals("get", result.action); + Assert.assertEquals("world", result.node.value); + Assert.assertNull(result.prevNode); + } + + @Test + public void getNonExistentKey() throws Exception { + String key = prefix + "/doesnotexist"; + + EtcdResult result; + + result = this.client.get(key); + Assert.assertNull(result); + } - @Before - public void initialize() { - this.prefix = "/unittest-" + UUID.randomUUID().toString(); - this.client = new EtcdClient(URI.create("http://127.0.0.1:4001/")); - } + @Test + public void testDelete() throws Exception { + String key = prefix + "/testDelete"; - @Test - public void setAndGet() throws Exception { - String key = prefix + "/message"; + EtcdResult result; - EtcdResult result; + result = this.client.set(key, "hello"); - result = this.client.set(key, "hello"); - Assert.assertEquals("set", result.action); - Assert.assertEquals("hello", result.node.value); - Assert.assertNull(result.prevNode); - - result = this.client.get(key); - Assert.assertEquals("get", result.action); - Assert.assertEquals("hello", result.node.value); - Assert.assertNull(result.prevNode); + result = this.client.get(key); + Assert.assertEquals("hello", result.node.value); - result = this.client.set(key, "world"); - Assert.assertEquals("set", result.action); - Assert.assertEquals("world", result.node.value); - Assert.assertNotNull(result.prevNode); - Assert.assertEquals("hello", result.prevNode.value); + result = this.client.delete(key); + Assert.assertEquals("delete", result.action); + Assert.assertEquals(null, result.node.value); + Assert.assertNotNull(result.prevNode); + Assert.assertEquals("hello", result.prevNode.value); - result = this.client.get(key); - Assert.assertEquals("get", result.action); - Assert.assertEquals("world", result.node.value); - Assert.assertNull(result.prevNode); - } - - @Test - public void getNonExistentKey() throws Exception { - String key = prefix + "/doesnotexist"; - - EtcdResult result; - - result = this.client.get(key); - Assert.assertNull(result); - } - - @Test - public void testDelete() throws Exception { - String key = prefix + "/testDelete"; - - EtcdResult result; - - result = this.client.set(key, "hello"); - - result = this.client.get(key); - Assert.assertEquals("hello", result.node.value); - - result = this.client.delete(key); - Assert.assertEquals("delete", result.action); - Assert.assertEquals(null, result.node.value); - Assert.assertNotNull(result.prevNode); - Assert.assertEquals("hello", result.prevNode.value); - - result = this.client.get(key); - Assert.assertNull(result); - } - - @Test - public void deleteNonExistentKey() throws Exception { - String key = prefix + "/doesnotexist"; - - try { - /*EtcdResult result =*/ this.client.delete(key); - Assert.fail(); - } catch (EtcdClientException e) { - Assert.assertTrue(e.isEtcdError(100)); - } - } - - @Test - public void testTtl() throws Exception { - String key = prefix + "/ttl"; - - EtcdResult result; - - result = this.client.set(key, "hello", 2); - Assert.assertNotNull(result.node.expiration); - Assert.assertTrue(result.node.ttl == 2 || result.node.ttl == 1); - - result = this.client.get(key); - Assert.assertEquals("hello", result.node.value); - - // TTL was redefined to mean TTL + 0.5s (Issue #306) - Thread.sleep(3000); - - result = this.client.get(key); - Assert.assertNull(result); - } - - @Test - public void testCAS() throws Exception { - String key = prefix + "/cas"; - - EtcdResult result; - - result = this.client.set(key, "hello"); - result = this.client.get(key); - Assert.assertEquals("hello", result.node.value); - - result = this.client.cas(key, "world", "world"); - Assert.assertEquals(true, result.isError()); - result = this.client.get(key); - Assert.assertEquals("hello", result.node.value); - - result = this.client.cas(key, "hello", "world"); - Assert.assertEquals(false, result.isError()); - result = this.client.get(key); - Assert.assertEquals("world", result.node.value); - } - - @Test - public void testWatchPrefix() throws Exception { - String key = prefix + "/watch"; - - EtcdResult result = this.client.set(key + "/f2", "f2"); - Assert.assertTrue(!result.isError()); - Assert.assertNotNull(result.node); - Assert.assertEquals("f2", result.node.value); - - ListenableFuture watchFuture = this.client.watch(key, - result.node.modifiedIndex + 1, - true); - try { - EtcdResult watchResult = watchFuture - .get(100, TimeUnit.MILLISECONDS); - Assert.fail("Subtree watch fired unexpectedly: " + watchResult); - } catch (TimeoutException e) { - // Expected - } - - Assert.assertFalse(watchFuture.isDone()); - - result = this.client.set(key + "/f1", "f1"); - Assert.assertTrue(!result.isError()); - Assert.assertNotNull(result.node); - Assert.assertEquals("f1", result.node.value); - - EtcdResult watchResult = watchFuture.get(100, TimeUnit.MILLISECONDS); - - Assert.assertNotNull(watchResult); - Assert.assertTrue(!watchResult.isError()); - Assert.assertNotNull(watchResult.node); - - { - Assert.assertEquals(key + "/f1", watchResult.node.key); - Assert.assertEquals("f1", watchResult.node.value); - Assert.assertEquals("set", watchResult.action); - Assert.assertNull(result.prevNode); - Assert.assertEquals(result.node.modifiedIndex, - watchResult.node.modifiedIndex); - } - } - - @Test - public void testList() throws Exception { - String key = prefix + "/dir"; - - EtcdResult result; - - result = this.client.set(key + "/f1", "f1"); - Assert.assertEquals("f1", result.node.value); - result = this.client.set(key + "/f2", "f2"); - Assert.assertEquals("f2", result.node.value); - result = this.client.set(key + "/f3", "f3"); - Assert.assertEquals("f3", result.node.value); - result = this.client.set(key + "/subdir1/f", "f"); - Assert.assertEquals("f", result.node.value); - - EtcdResult listing = this.client.listChildren(key); - Assert.assertEquals(4, listing.node.nodes.size()); - Assert.assertEquals("get", listing.action); - - { - EtcdNode child = listing.node.nodes.get(0); - Assert.assertEquals(key + "/f1", child.key); - Assert.assertEquals("f1", child.value); - Assert.assertEquals(false, child.dir); - } - { - EtcdNode child = listing.node.nodes.get(1); - Assert.assertEquals(key + "/f2", child.key); - Assert.assertEquals("f2", child.value); - Assert.assertEquals(false, child.dir); - } - { - EtcdNode child = listing.node.nodes.get(2); - Assert.assertEquals(key + "/f3", child.key); - Assert.assertEquals("f3", child.value); - Assert.assertEquals(false, child.dir); - } - { - EtcdNode child = listing.node.nodes.get(3); - Assert.assertEquals(key + "/subdir1", child.key); - Assert.assertEquals(null, child.value); - Assert.assertEquals(true, child.dir); - } - } - - @Test - public void testGetVersion() throws Exception { - String version = this.client.getVersion(); - Assert.assertTrue(version.startsWith("etcd 0.")); - } + result = this.client.get(key); + Assert.assertNull(result); + } + + @Test + public void deleteNonExistentKey() throws Exception { + String key = prefix + "/doesnotexist"; + + try { + /*EtcdResult result =*/ this.client.delete(key); + Assert.fail(); + } catch (EtcdClientException e) { + Assert.assertTrue(e.isEtcdError(100)); + } + } + + @Test + public void testTtl() throws Exception { + String key = prefix + "/ttl"; + + EtcdResult result; + + result = this.client.set(key, "hello", 2); + Assert.assertNotNull(result.node.expiration); + Assert.assertTrue(result.node.ttl == 2 || result.node.ttl == 1); + + result = this.client.get(key); + Assert.assertEquals("hello", result.node.value); + + // TTL was redefined to mean TTL + 0.5s (Issue #306) + Thread.sleep(3000); + + result = this.client.get(key); + Assert.assertNull(result); + } + + @Test + public void testCAS() throws Exception { + String key = prefix + "/cas"; + + EtcdResult result; + + result = this.client.set(key, "hello"); + result = this.client.get(key); + Assert.assertEquals("hello", result.node.value); + + result = this.client.cas(key, "world", "world"); + Assert.assertEquals(true, result.isError()); + result = this.client.get(key); + Assert.assertEquals("hello", result.node.value); + + result = this.client.cas(key, "hello", "world"); + Assert.assertEquals(false, result.isError()); + result = this.client.get(key); + Assert.assertEquals("world", result.node.value); + } + + @Test + public void testWatchPrefix() throws Exception { + String key = prefix + "/watch"; + + EtcdResult result = this.client.set(key + "/f2", "f2"); + Assert.assertTrue(!result.isError()); + Assert.assertNotNull(result.node); + Assert.assertEquals("f2", result.node.value); + + ListenableFuture watchFuture = this.client.watch(key, + result.node.modifiedIndex + 1, + true); + try { + EtcdResult watchResult = watchFuture + .get(100, TimeUnit.MILLISECONDS); + Assert.fail("Subtree watch fired unexpectedly: " + watchResult); + } catch (TimeoutException e) { + // Expected + } + + Assert.assertFalse(watchFuture.isDone()); + + result = this.client.set(key + "/f1", "f1"); + Assert.assertTrue(!result.isError()); + Assert.assertNotNull(result.node); + Assert.assertEquals("f1", result.node.value); + + EtcdResult watchResult = watchFuture.get(100, TimeUnit.MILLISECONDS); + + Assert.assertNotNull(watchResult); + Assert.assertTrue(!watchResult.isError()); + Assert.assertNotNull(watchResult.node); + + { + Assert.assertEquals(key + "/f1", watchResult.node.key); + Assert.assertEquals("f1", watchResult.node.value); + Assert.assertEquals("set", watchResult.action); + Assert.assertNull(result.prevNode); + Assert.assertEquals(result.node.modifiedIndex, + watchResult.node.modifiedIndex); + } + } + + @Test + public void testList() throws Exception { + String key = prefix + "/dir"; + + EtcdResult result; + + result = this.client.set(key + "/f1", "f1"); + Assert.assertEquals("f1", result.node.value); + result = this.client.set(key + "/f2", "f2"); + Assert.assertEquals("f2", result.node.value); + result = this.client.set(key + "/f3", "f3"); + Assert.assertEquals("f3", result.node.value); + result = this.client.set(key + "/subdir1/f", "f"); + Assert.assertEquals("f", result.node.value); + + EtcdResult listing = this.client.listChildren(key); + Assert.assertEquals(4, listing.node.nodes.size()); + Assert.assertEquals("get", listing.action); + + { + EtcdNode child = listing.node.nodes.get(0); + Assert.assertEquals(key + "/f1", child.key); + Assert.assertEquals("f1", child.value); + Assert.assertEquals(false, child.dir); + } + { + EtcdNode child = listing.node.nodes.get(1); + Assert.assertEquals(key + "/f2", child.key); + Assert.assertEquals("f2", child.value); + Assert.assertEquals(false, child.dir); + } + { + EtcdNode child = listing.node.nodes.get(2); + Assert.assertEquals(key + "/f3", child.key); + Assert.assertEquals("f3", child.value); + Assert.assertEquals(false, child.dir); + } + { + EtcdNode child = listing.node.nodes.get(3); + Assert.assertEquals(key + "/subdir1", child.key); + Assert.assertEquals(null, child.value); + Assert.assertEquals(true, child.dir); + } + } + + @Test + public void testGetVersion() throws Exception { + String version = this.client.getVersion(); + Assert.assertTrue(version.startsWith("etcd 0.")); + } @Test public void testResponseMetadata() throws Exception {