Ktor¶
etcd-recipes-ktor is an application plugin that connects to etcd for the server's
lifetime and hands the connection to your routes. It targets Ktor 3.5.x.
Kotlin only
Ktor is a Kotlin framework, and the plugin's consumer API is a pair of extension
properties on Application. There is no Java tab on this page because there is no
sensible Java story — Java callers on a JVM server stack want the
Spring Boot starter, or plain connectToEtcd and an EtcdRecipes
of their own.
Installing¶
EtcdPluginConfig is small on purpose — it is the subset of
EtcdConnectionConfig that a plugin block can express
plainly:
| Field | Type | Notes |
|---|---|---|
endpoints |
List<String> |
Empty by default; set it unless you inject a client |
user |
String? |
|
password |
String? |
|
namespace |
String? |
Prefixes every key this client touches |
client |
Client? |
A pre-built client — see ownership, below |
install(EtcdPlugin) {
endpoints = ["http://etcd-1:2379", "http://etcd-2:2379"]
user = "orders-svc"
password = System.getenv("ETCD_PASSWORD")
namespace = "/myapp/"
}
The plugin builds its client through connectToEtcd, so it inherits the recipe defaults
(a 5-second connect timeout and a 30-second retry ceiling) described in
Connecting. connectTimeout, retryMaxDuration, and
TLS are not exposed in the plugin block — if you need them, build the client yourself and
inject it.
Who closes the client¶
This is the one rule to get right, and it is decided entirely by which fields you set.
Set connection fields and the plugin owns the client. Inject one and it doesn't.
Supply endpoints (etc.) — the plugin builds the client, and closes it on
ApplicationStopping. You do nothing.
Supply client — the plugin installs it and never closes it, because it did not
create it. Closing it is your job, and if you forget, the connection outlives the
server.
The plugin will not guess. It closes exactly what it created and nothing else, which is the only rule that is safe when the injected client is shared with the rest of your application — closing a client out from under its other users would be far worse than leaking one.
// Injecting a client makes the plugin a pure consumer: it will NOT close this client on
// ApplicationStopping, because it did not create it. Closing it stays your job.
val myClient = connectToEtcd(["http://localhost:2379"])
install(EtcdPlugin) {
client = myClient
}
Inject a client when you need builder options the plugin block does not model (TLS, timeouts, interceptors), when the client is shared with non-Ktor code, or when a test needs to hand in a fake.
Using it¶
Two extension properties on Application:
etcdClient is the raw jetcd client the plugin installed, for the extension-level API
(putValue, watcher, transactions):
// The raw jetcd client the plugin installed, for the extension-level API.
val client: Client = etcdClient
logger.info { "Connected: $client" }
etcdRecipes is an EtcdRecipes factory bound to that client:
// A factory bound to the plugin's client. Each call news up a recipe; you own its
// start()/close() lifecycle, so scope it to the work rather than to the application.
etcdRecipes.mutex("/locks/orders").use { mutex ->
mutex.withLock {
logger.info { "One instance of this service at a time" }
}
}
etcdRecipes allocates on every access
It is a get() that news up an EtcdRecipes each time, not a cached instance. That
is cheap — the factory holds a client reference and nothing else — but it means the
identity is not stable, so do not use it as a map key or compare it by reference.
etcdClient is stable; it is stored in the application's attributes.
The recipes it builds are not owned by the plugin either. As everywhere else,
start()/close() is yours: scope short-lived recipes to the request or the unit of
work, and hold long-lived ones (a cache, a LeaderLatch) yourself, closing them on
ApplicationStopping alongside the plugin's own teardown.