etcd basics¶
The recipes in this library are layered on a thin Kotlin extension layer over jetcd — they are not a wrapper around it. That distinction is the design, not a detail:
- Nothing is hidden. A
Clientis jetcd'sClient, aGetResponseis jetcd'sGetResponse. You never hold a proxy object, and you never have to escape the library to reach etcd's real API. - The layer is additive. Everything in
io.etcd.recipes.commonis an extension function or property, so it composes with jetcd rather than shadowing it. Where jetcd is already pleasant, there is nothing here to learn. - It earns its place at the edges: retry and timeout policy,
CompletableFutureunwrapping, byte marshalling, and builder ergonomics. Those are the things every recipe would otherwise reimplement.
common/ is where nearly all direct etcd interaction happens. A new recipe should compose
these extensions rather than reach into jetcd itself — that is what keeps resilience policy,
metrics, and error semantics consistent across every recipe instead of scattered through a
dozen of them.
You may not need the recipes¶
These four pages are worth reading even if you only came for the recipes, because every recipe is built out of exactly what is described here. A leader election is a lease plus a transaction plus a watch. Knowing what those three do is what makes the recipes' failure modes legible rather than mysterious.
They are also enough on their own. Plenty of coordination problems are one transaction, and
reaching for DistributedMutex when a CAS would do is the expensive way to solve them.
| Page | Covers |
|---|---|
| Key/value | Puts, gets, deletes, children, and the byte-marshalling helpers |
| Watches | Streaming change notifications, and the callback rule you cannot break |
| Transactions | etcd's atomic if/then/else, and the basis of every CAS recipe |
| Leases | TTLs, keep-alives, and the mechanism under every ephemeral key |
Why every file has a @JvmName¶
Kotlin compiles top-level functions into a class named after the file — KVExtensions.kt
would become KVExtensionsKt. That is an unpleasant name to type from Java, and worse, it
is an implementation detail that would change if the file were ever renamed.
So every extension file declares one:
This is precisely what makes the layer usable from Java. client.putValue("/key", "value")
in Kotlin is KVUtils.putValue(client, "/key", "value") in Java — the receiver becomes the
first argument, and the facade class is a name that was chosen rather than derived:
| Source file | Java facade | Covers |
|---|---|---|
ClientExtensions.kt |
ClientUtils |
connectToEtcd, ping, recipe client defaults |
KVExtensions.kt |
KVUtils |
putValue, getValue, deleteKey, compact, key presence |
TypedKVExtensions.kt |
TypedKVUtils |
codec-based putValue / getValue |
ChildrenExtensions.kt |
ChildrenUtils |
getChildren, getChildCount, deleteChildren |
WatchExtensions.kt |
WatchUtils |
watcher, withWatcher, watcherWithLatch |
TxnExtensions.kt |
TxnUtils |
transaction, comparisons, setTo, deleteOp |
LeaseExtensions.kt |
LeaseUtils |
leaseGrant, leaseRevoke, keepAlive |
KeepAliveExtensions.kt |
KeepAliveUtils |
putValueWithKeepAlive, putValuesWithKeepAlive |
LockExtensions.kt |
LockUtils |
raw pass-through to etcd's lock service |
BuilderExtensions.kt |
BuilderUtils |
getOption { }, putOption { }, watchOption { }, … |
ByteSequenceExtensions.kt |
ByteSequenceUtils |
asByteSequence, asString, asInt, asLong |
KeyValueExtensions.kt |
KeyValueUtils |
KeyValue → Pair conversions |
PairExtensions.kt |
PairUtils |
keys, values, bulk pair decoding |
PathExtensions.kt |
PathUtils |
appendToPath |
Extension properties become get-prefixed statics
An extension property has no argument list to become a first parameter, so Kotlin
exposes it as a getter. bytes.asString in Kotlin is ByteSequenceUtils.getAsString(bytes)
in Java, and "/key".doesExist is TxnUtils.getDoesExist("/key"). It reads oddly the
first time and then never surprises you again. See the Java guide.
Resilience is a parameter, not a mode¶
Almost every extension here takes a trailing rpc: RpcResilience argument that defaults to
RpcResilience.DEFAULT — a retry policy plus an operation timeout. You will see it in the
signatures and can safely ignore it until you need to change it; the snippets on these pages
never pass it, which is the common case.
Two deliberate exceptions are worth knowing up front, because they encode judgements you would otherwise have to make yourself:
transaction { }is never retried. A failed commit is ambiguous — it may have applied — so retrying it is a correctness decision that belongs to the caller, not the transport.lockdefaults toRpcResilience.DISABLED. A lock call legitimately waits server-side for the current holder, and a 30-second operation timeout would abort valid waits.
The full story, including watch and lease recovery, is on Resilience.
Where to go next¶
If you have not connected yet, start with Connecting. If you want the recipe-level lifecycle and exception model, read Core concepts.