diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/KV.java b/jetcd-core/src/main/java/io/etcd/jetcd/KV.java index a2429b957..4fd2d2d47 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/KV.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/KV.java @@ -26,6 +26,7 @@ import io.etcd.jetcd.options.DeleteOption; import io.etcd.jetcd.options.GetOption; import io.etcd.jetcd.options.PutOption; +import io.etcd.jetcd.options.TxnOption; import io.etcd.jetcd.support.CloseableClient; /** @@ -115,4 +116,12 @@ public interface KV extends CloseableClient { * @return a Txn */ Txn txn(); + + /** + * creates a transaction. + * + * @param option TxnOption + * @return a Txn + */ + Txn txn(TxnOption option); } diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/impl/ElectionImpl.java b/jetcd-core/src/main/java/io/etcd/jetcd/impl/ElectionImpl.java index 9d2de9407..60fd11a0b 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/impl/ElectionImpl.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/impl/ElectionImpl.java @@ -88,7 +88,7 @@ public CompletableFuture campaign(ByteSequence electionName, l execute( () -> stubWithLeader().campaign(request), CampaignResponse::new, - Errors::isRetryable)); + Errors::isRetryableForNoSafeRedoOp)); } @Override @@ -111,7 +111,7 @@ public CompletableFuture proclaim(LeaderKey leaderKey, ByteSeq execute( () -> stubWithLeader().proclaim(request), ProclaimResponse::new, - Errors::isRetryable)); + Errors::isRetryableForNoSafeRedoOp)); } @Override @@ -126,7 +126,7 @@ public CompletableFuture leader(ByteSequence electionName) { execute( () -> stubWithLeader().leader(request), response -> new LeaderResponse(response, namespace), - Errors::isRetryable)); + Errors::isRetryableForNoSafeRedoOp)); } @Override @@ -162,7 +162,7 @@ public CompletableFuture resign(LeaderKey leaderKey) { execute( () -> stubWithLeader().resign(request), ResignResponse::new, - Errors::isRetryable)); + Errors::isRetryableForNoSafeRedoOp)); } private CompletableFuture wrapConvertException(CompletableFuture future) { diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/impl/Impl.java b/jetcd-core/src/main/java/io/etcd/jetcd/impl/Impl.java index 5ab236611..12aec9a05 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/impl/Impl.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/impl/Impl.java @@ -89,9 +89,11 @@ protected CompletableFuture completable( */ protected CompletableFuture execute( Supplier> supplier, - Function resultConvert) { + Function resultConvert, + boolean autoRetry) { - return execute(supplier, resultConvert, Errors::isRetryable); + return execute(supplier, resultConvert, + autoRetry ? Errors::isRetryableForSafeRedoOp : Errors::isRetryableForNoSafeRedoOp); } /** diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/impl/KVImpl.java b/jetcd-core/src/main/java/io/etcd/jetcd/impl/KVImpl.java index 4baf11e1e..868ef4119 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/impl/KVImpl.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/impl/KVImpl.java @@ -33,6 +33,7 @@ import io.etcd.jetcd.options.DeleteOption; import io.etcd.jetcd.options.GetOption; import io.etcd.jetcd.options.PutOption; +import io.etcd.jetcd.options.TxnOption; import io.etcd.jetcd.support.Errors; import io.etcd.jetcd.support.Requests; @@ -65,7 +66,7 @@ public CompletableFuture put(ByteSequence key, ByteSequence value, return execute( () -> stub.put(Requests.mapPutRequest(key, value, option, namespace)), response -> new PutResponse(response, namespace), - Errors::isRetryable); + option.isAutoRetry() ? Errors::isRetryableForSafeRedoOp : Errors::isRetryableForNoSafeRedoOp); } @Override @@ -81,7 +82,7 @@ public CompletableFuture get(ByteSequence key, GetOption option) { return execute( () -> stub.range(Requests.mapRangeRequest(key, option, namespace)), response -> new GetResponse(response, namespace), - Errors::isRetryable); + Errors::isRetryableForSafeRedoOp); } @Override @@ -97,7 +98,7 @@ public CompletableFuture delete(ByteSequence key, DeleteOption o return execute( () -> stub.deleteRange(Requests.mapDeleteRequest(key, option, namespace)), response -> new DeleteResponse(response, namespace), - Errors::isRetryable); + option.isAutoRetry() ? Errors::isRetryableForSafeRedoOp : Errors::isRetryableForNoSafeRedoOp); } @Override @@ -116,15 +117,21 @@ public CompletableFuture compact(long rev, CompactOption option return execute( () -> stub.compact(request), CompactResponse::new, - Errors::isRetryable); + Errors::isRetryableForSafeRedoOp); } @Override public Txn txn() { + return txn(TxnOption.DEFAULT); + } + + @Override + public Txn txn(TxnOption option) { return TxnImpl.newTxn( request -> execute( () -> stub.txn(request), - response -> new TxnResponse(response, namespace), Errors::isRetryable), + response -> new TxnResponse(response, namespace), + option.isAutoRetry() ? Errors::isRetryableForSafeRedoOp : Errors::isRetryableForNoSafeRedoOp), namespace); } } diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/impl/LeaseImpl.java b/jetcd-core/src/main/java/io/etcd/jetcd/impl/LeaseImpl.java index 67d9ac04a..6a94cb314 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/impl/LeaseImpl.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/impl/LeaseImpl.java @@ -84,7 +84,8 @@ public CompletableFuture grant(long ttl) { LeaseGrantRequest.newBuilder() .setTTL(ttl) .build()), - LeaseGrantResponse::new); + LeaseGrantResponse::new, + true); } @Override @@ -94,7 +95,8 @@ public CompletableFuture grant(long ttl, long timeout, TimeU LeaseGrantRequest.newBuilder() .setTTL(ttl) .build()), - LeaseGrantResponse::new); + LeaseGrantResponse::new, + true); } @Override @@ -104,7 +106,8 @@ public CompletableFuture revoke(long leaseId) { LeaseRevokeRequest.newBuilder() .setID(leaseId) .build()), - LeaseRevokeResponse::new); + LeaseRevokeResponse::new, + true); } @Override @@ -118,7 +121,8 @@ public CompletableFuture timeToLive(long leaseId, Lease return execute( () -> this.stub.leaseTimeToLive(leaseTimeToLiveRequest), - LeaseTimeToLiveResponse::new); + LeaseTimeToLiveResponse::new, + true); } @Override diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/impl/LockImpl.java b/jetcd-core/src/main/java/io/etcd/jetcd/impl/LockImpl.java index 4518082f7..452fa5201 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/impl/LockImpl.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/impl/LockImpl.java @@ -69,7 +69,7 @@ public CompletableFuture lock(ByteSequence name, long leaseId) { return execute( () -> stubWithLeader().lock(request), response -> new LockResponse(response, namespace), - Errors::isRetryable); + Errors::isRetryableForSafeRedoOp); } @Override @@ -83,6 +83,6 @@ public CompletableFuture unlock(ByteSequence lockKey) { return execute( () -> stubWithLeader().unlock(request), UnlockResponse::new, - Errors::isRetryable); + Errors::isRetryableForSafeRedoOp); } } diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/kv/GetResponse.java b/jetcd-core/src/main/java/io/etcd/jetcd/kv/GetResponse.java index 88120ade5..6f2788045 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/kv/GetResponse.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/kv/GetResponse.java @@ -59,6 +59,10 @@ public boolean isMore() { /** * Returns the number of keys within the range when requested. + * Note this value is never affected by filtering options (limit, min or max created or modified revisions) + * Count is the count for keys on the range part of a request. + * Filters for limit and created or modified revision ranges restrict the + * returned KVs, but not the count. * * @return count. */ diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/options/DeleteOption.java b/jetcd-core/src/main/java/io/etcd/jetcd/options/DeleteOption.java index fb1113824..bee61258e 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/options/DeleteOption.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/options/DeleteOption.java @@ -29,11 +29,13 @@ public final class DeleteOption { private final ByteSequence endKey; private final boolean prevKV; private final boolean prefix; + private final boolean autoRetry; - private DeleteOption(ByteSequence endKey, boolean prevKV, boolean prefix) { + private DeleteOption(ByteSequence endKey, boolean prevKV, boolean prefix, final boolean autoRetry) { this.endKey = endKey; this.prevKV = prevKV; this.prefix = prefix; + this.autoRetry = autoRetry; } public Optional getEndKey() { @@ -49,10 +51,25 @@ public boolean isPrevKV() { return prevKV; } + /** + * Whether to treat this deletion as deletion by prefix + * + * @return true if deletion by prefix. + */ public boolean isPrefix() { return prefix; } + /** + * Whether to treat a delete operation as idempotent from the point of view of automated retries. + * Note under failure scenarios this may mean a single delete is attempted more than once. + * + * @return true if automated retries should happen. + */ + public boolean isAutoRetry() { + return autoRetry; + } + /** * Returns the builder. * @@ -65,6 +82,11 @@ public static Builder newBuilder() { return builder(); } + /** + * Returns the builder. + * + * @return the builder + */ public static Builder builder() { return new Builder(); } @@ -73,6 +95,7 @@ public static final class Builder { private ByteSequence endKey; private boolean prevKV = false; private boolean prefix = false; + private boolean autoRetry = false; private Builder() { } @@ -144,8 +167,22 @@ public Builder withPrevKV(boolean prevKV) { return this; } + /** + * When autoRetry is set, the delete operation is treated as idempotent from the point of view of automated retries. + * Note under some failure scenarios true may make a delete operation be attempted more than once, where + * a first attempt succeeded but its result did not reach the client; by default (autoRetry=false), + * the client won't retry since it is not safe to assume on such a failure the operation did not happen. + * Requesting withAutoRetry means the client is explicitly asking for retry nevertheless. + * + * @return builder + */ + public Builder withAutoRetry() { + this.autoRetry = true; + return this; + } + public DeleteOption build() { - return new DeleteOption(endKey, prevKV, prefix); + return new DeleteOption(endKey, prevKV, prefix, autoRetry); } } diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/options/GetOption.java b/jetcd-core/src/main/java/io/etcd/jetcd/options/GetOption.java index c638ba7d5..ec0abe8e6 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/options/GetOption.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/options/GetOption.java @@ -20,6 +20,7 @@ import io.etcd.jetcd.ByteSequence; import io.etcd.jetcd.KV; +import io.etcd.jetcd.kv.GetResponse; import static java.util.Objects.requireNonNull; @@ -76,56 +77,158 @@ private GetOption( /** * Get the maximum number of keys to return for a get request. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + *

+ * + * * @return the maximum number of keys to return. */ public long getLimit() { return this.limit; } + /** + * Get the end key for a range request + * + * @return the end key for a range request + */ public Optional getEndKey() { return Optional.ofNullable(this.endKey); } + /** + * Get the revision for the request + * + * @return the revision for the request + */ public long getRevision() { return revision; } + /** + * Get the sort order for the request + * + * @return the sort order for the request + */ public SortOrder getSortOrder() { return sortOrder; } + /** + * Get the sort field for the request + * + * @return the sort field for the request + */ public SortTarget getSortField() { return sortTarget; } + /** + * Return if the consistency level for this request is "serializable". + * Note serializable is a lower than default consistency, and implies + * the possibility of getting stale data. + * + * @return true if this request is only serializable consistency + */ public boolean isSerializable() { return serializable; } + /** + * True if this request should get only keys in a range and there is no + * need to retrieve the values. + * + * @return true if only get keys + */ public boolean isKeysOnly() { return keysOnly; } + /** + * True if this request should only populate the count of keys matching + * a range, and no other data. + * + * @return true if only get the count of keys + */ public boolean isCountOnly() { return countOnly; } + /** + * Only populate results for keys that match a + * minimum value for a created revision. + * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting a min create revision option + * with the count only option. + *

+ * + * @return minimum created revision to match, or zero for any. + */ public long getMinCreateRevision() { return this.minCreateRevision; } + /** + * Only populate results for keys that match a + * maximum value for a created revision. + * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting a max create revision option + * with the count only option. + *

+ * + * @return maximum created revision to match, or zero for any. + */ public long getMaxCreateRevision() { return this.maxCreateRevision; } + /** + * Only populate results for keys that match a + * minimum value for a modified revision. + * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting a min mod revision option + * with the count only option. + *

+ * + * @return minimum modified revision to match, or zero for any. + */ public long getMinModRevision() { return this.minModRevision; } + /** + * Only populate results for keys that match a + * maximum value for a modified revision. + * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting a max mod revision option + * with the count only option. + *

+ * + * @return maximum modified revision to match, or zero for any. + */ public long getMaxModRevision() { return this.maxModRevision; } + /** + * True if this Get request should do prefix match + * + * @return true if this Get request should do prefix match + */ public boolean isPrefix() { return prefix; } @@ -176,6 +279,13 @@ private Builder() { /** * Limit the number of keys to return for a get request. By default is 0 - no limitation. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting this option + * with the count only option. + *

+ * * @param limit the maximum number of keys to return for a get request. * @return builder */ @@ -229,7 +339,9 @@ public Builder withSortField(SortTarget field) { *

* Get requests are linearizable by * default. For better performance, a serializable get request is served locally without needing - * to reach consensus with other nodes in the cluster. + * to reach consensus with other nodes in the cluster. Note this is a tradeoff with strict + * consistency so it should be used with care in situations where reading stale + * is acceptable. * * @param serializable is the get request a serializable get request. * @return builder @@ -316,10 +428,17 @@ public Builder withPrefix(ByteSequence prefix) { } /** - * Limit returned keys to those with create revision greater than the provided value. + * Limit returned keys to those with create revision greater or equal than the provided value. * min_create_revision is the lower bound for returned key create revisions; all keys with * lesser create revisions will be filtered away. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting this option + * with the count only option. + *

+ * * @param createRevision create revision * @return builder */ @@ -329,10 +448,17 @@ public Builder withMinCreateRevision(long createRevision) { } /** - * Limit returned keys to those with create revision less than the provided value. + * Limit returned keys to those with create revision less or equal than the provided value. * max_create_revision is the upper bound for returned key create revisions; all keys with * greater create revisions will be filtered away. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting this option + * with the count only option. + *

+ * * @param createRevision create revision * @return builder */ @@ -342,10 +468,17 @@ public Builder withMaxCreateRevision(long createRevision) { } /** - * Limit returned keys to those with mod revision greater than the provided value. + * Limit returned keys to those with mod revision greater or equal than the provided value. * min_mod_revision is the lower bound for returned key mod revisions; all keys with lesser mod * revisions will be filtered away. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting this option + * with the count only option. + *

+ * * @param modRevision mod revision * @return this builder instance */ @@ -355,10 +488,17 @@ public Builder withMinModRevision(long modRevision) { } /** - * Limit returned keys to those with mod revision less than the provided value. max_mod_revision + * Limit returned keys to those with mod revision less or equal than the provided value. max_mod_revision * is the upper bound for returned key mod revisions; all keys with greater mod revisions will * be filtered away. * + *

+ * Note this filter does not affect the count field in GetResponse. + * {@link GetResponse#getCount()} always counts the number of keys matched on a range, independent of filters. + * For the same reason, it would be meaningless to mix setting this option + * with the count only option. + *

+ * * @param modRevision mod revision * @return this builder instance */ diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/options/PutOption.java b/jetcd-core/src/main/java/io/etcd/jetcd/options/PutOption.java index ca5565f03..626d2a808 100644 --- a/jetcd-core/src/main/java/io/etcd/jetcd/options/PutOption.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/options/PutOption.java @@ -26,10 +26,12 @@ public final class PutOption { private final long leaseId; private final boolean prevKV; + private final boolean autoRetry; - private PutOption(long leaseId, boolean prevKV) { + private PutOption(long leaseId, boolean prevKV, boolean autoRetry) { this.leaseId = leaseId; this.prevKV = prevKV; + this.autoRetry = autoRetry; } /** @@ -50,6 +52,16 @@ public boolean getPrevKV() { return this.prevKV; } + /** + * Whether to treat a put operation as idempotent from the point of view of automated retries. + * Note under failure scenarios this may mean a single put executes more than once. + * + * @return true if automated retries should happen. + */ + public boolean isAutoRetry() { + return autoRetry; + } + /** * Returns the builder. * @@ -73,6 +85,7 @@ public static final class Builder { private long leaseId = 0L; private boolean prevKV = false; + private boolean autoRetry = false; private Builder() { } @@ -100,13 +113,28 @@ public Builder withPrevKV() { return this; } + /** + * When autoRetry is set, treat this put as idempotent from the point of view of automated retries. + * Note under some failure scenarios autoRetry=true may make a put operation execute more than once, where + * a first attempt succeeded but its result did not reach the client; by default (autoRetry=false), + * the client won't retry since it is not safe to assume on such a failure that the operation did not happen + * in the server. + * Requesting withAutoRetry means the client is explicitly asking for retry nevertheless. + * + * @return builder + */ + public Builder withAutoRetry() { + this.autoRetry = true; + return this; + } + /** * build the put option. * * @return the put option */ public PutOption build() { - return new PutOption(this.leaseId, this.prevKV); + return new PutOption(this.leaseId, this.prevKV, this.autoRetry); } } diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/options/TxnOption.java b/jetcd-core/src/main/java/io/etcd/jetcd/options/TxnOption.java new file mode 100644 index 000000000..8d2ff1e96 --- /dev/null +++ b/jetcd-core/src/main/java/io/etcd/jetcd/options/TxnOption.java @@ -0,0 +1,54 @@ +package io.etcd.jetcd.options; + +public final class TxnOption { + public static final TxnOption DEFAULT = builder().build(); + + private final boolean autoRetry; + + private TxnOption(final boolean autoRetry) { + this.autoRetry = autoRetry; + } + + /** + * Whether to treat a txn operation as idempotent from the point of view of automated retries. + * + * @return true if automated retries should happen. + */ + public boolean isAutoRetry() { + return autoRetry; + } + + /** + * Returns the builder. + * + * @return the builder + */ + public static TxnOption.Builder builder() { + return new TxnOption.Builder(); + } + + public static final class Builder { + private boolean autoRetry = false; + + private Builder() { + } + + /** + * When autoRetry is set, the txn operation is treated as idempotent from the point of view of automated retries. + * Note under some failure scenarios true may make a txn operation be attempted and/or execute more than once, where + * a first attempt executed but its result status did not reach the client; by default (autoRetry=false), + * the client won't retry since it is not safe to assume on such a failure the operation did not happen. + * Requesting withAutoRetry means the client is explicitly asking for retry nevertheless. + * + * @return builder + */ + public Builder withAutoRetry() { + this.autoRetry = true; + return this; + } + + public TxnOption build() { + return new TxnOption(autoRetry); + } + } +} diff --git a/jetcd-core/src/main/java/io/etcd/jetcd/support/Errors.java b/jetcd-core/src/main/java/io/etcd/jetcd/support/Errors.java index 2b825aad8..9e6b3b958 100755 --- a/jetcd-core/src/main/java/io/etcd/jetcd/support/Errors.java +++ b/jetcd-core/src/main/java/io/etcd/jetcd/support/Errors.java @@ -26,9 +26,18 @@ public final class Errors { private Errors() { } - public static boolean isRetryable(Status status) { - return Status.UNAVAILABLE.getCode().equals(status.getCode()) || isInvalidTokenError(status) - || isAuthStoreExpired(status); + // isRetryable implementation for idempotent operations. + public static boolean isRetryableForSafeRedoOp(Status status) { + return Status.UNAVAILABLE.getCode().equals(status.getCode()) || isAlwaysSafeToRetry(status); + } + + // isRetryable implementation for non-idempotent operations + public static boolean isRetryableForNoSafeRedoOp(Status status) { + return isAlwaysSafeToRetry(status); + } + + public static boolean isAlwaysSafeToRetry(Status status) { + return isInvalidTokenError(status) || isAuthStoreExpired(status); } public static boolean isInvalidTokenError(Throwable e) { diff --git a/jetcd-core/src/test/java/io/etcd/jetcd/impl/KVTest.java b/jetcd-core/src/test/java/io/etcd/jetcd/impl/KVTest.java index d7a675e76..54ded810e 100644 --- a/jetcd-core/src/test/java/io/etcd/jetcd/impl/KVTest.java +++ b/jetcd-core/src/test/java/io/etcd/jetcd/impl/KVTest.java @@ -316,7 +316,7 @@ public void testKVClientCanRetryPutOnEtcdRestart() throws InterruptedException { for (int i = 0; i < putCount; ++i) { ByteSequence value = ByteSequence .from(Integer.toString(i), StandardCharsets.UTF_8); - customClient.getKVClient().put(key, value).join(); + customClient.getKVClient().put(key, value, PutOption.builder().withAutoRetry().build()).join(); } }); diff --git a/jetcd-core/src/test/java/io/etcd/jetcd/impl/UtilTest.java b/jetcd-core/src/test/java/io/etcd/jetcd/impl/UtilTest.java index 6ba039aea..e28f4c134 100644 --- a/jetcd-core/src/test/java/io/etcd/jetcd/impl/UtilTest.java +++ b/jetcd-core/src/test/java/io/etcd/jetcd/impl/UtilTest.java @@ -39,16 +39,18 @@ public void testAuthStoreExpired() { } @Test - public void testAuthErrorIsNotRetryable() { + public void testAuthErrorIsRetryable() { Status authErrorStatus = Status.UNAUTHENTICATED .withDescription("etcdserver: invalid auth token"); Status status = Status.fromThrowable(new StatusException(authErrorStatus)); - assertThat(Errors.isRetryable(status)).isTrue(); + assertThat(Errors.isRetryableForNoSafeRedoOp(status)).isTrue(); + assertThat(Errors.isRetryableForSafeRedoOp(status)).isTrue(); } @Test public void testUnavailableErrorIsRetryable() { Status status = Status.fromThrowable(new StatusException(Status.UNAVAILABLE)); - assertThat(Errors.isRetryable(status)).isTrue(); + assertThat(Errors.isRetryableForNoSafeRedoOp(status)).isFalse(); + assertThat(Errors.isRetryableForSafeRedoOp(status)).isTrue(); } }