Contributing¶
Source: github.com/pambrose/etcd-recipes. Licensed Apache 2.0.
Building¶
JDK 17 and the Gradle wrapper. make help lists every target.
make build # ./gradlew clean build -x test
make tests # full suite against a local etcd at localhost:2379
make tests-tc # full suite against an ephemeral Testcontainers etcd (needs Docker)
make all-tests # local, Testcontainers and multi-container variants in sequence
make lint # ./gradlew lintKotlin detekt
make coverage # Kover HTML + XML + summary
make kdocs # Dokka API docs
make tests expects an etcd at http://localhost:2379:
./etcd-start.sh # start (or: make etcd-start)
./etcd-stop.sh # graceful shutdown — SIGTERM, then SIGKILL after 10s
A single test class:
Modules¶
| Module | Published as |
|---|---|
etcd-recipes-core |
com.pambrose:etcd-recipes-core |
etcd-recipes-jackson |
com.pambrose:etcd-recipes-jackson |
etcd-recipes-micrometer |
com.pambrose:etcd-recipes-micrometer |
etcd-recipes-spring-boot-starter |
com.pambrose:etcd-recipes-spring-boot-starter |
etcd-recipes-ktor |
com.pambrose:etcd-recipes-ktor |
etcd-recipes-examples |
— runnable examples |
etcd-recipes-test-runners |
— test-only shadow JAR |
Tests¶
Thread-based (*Tests.kt beside each recipe) — N threads in one JVM simulate
distributed clients via blockingThreads(...) / nonblockingThreads(...) from
TestExtensions.kt.
Container-based (container/Container*Test.kt) — each participant runs in its own
container against a shared etcd container. Gated by assumeTrue(testcontainers=true), so a
default ./gradlew check skips them.
Test JVMs fork per class (forkEvery = 1) so one spec's background threads and watch
connections cannot bleed into the next. maxParallelForks lets classes run concurrently
against one etcd, so each test namespaces its keys by class name. Keep doing that.
New Kotlin tests use Kotest with StringSpec() and an init {}
block, plus MockK where it helps.
Adding a recipe¶
- Compose the existing extensions in
common/rather than reaching into jetcd directly. - Extend
EtcdConnectorfor anything stateful — it gives you the lifecycle, the exception list, connection state andclose(). - Raise
EtcdRecipeException/EtcdRecipeRuntimeException, not jetcd types. - Add Kotest tests under
etcd-recipes-core/src/test/kotlin/. - Add a runnable example under
etcd-recipes-examples/. - Document it here, with a compiled snippet (below).
Documentation¶
This site is Zensical, under website/.
make site # serve locally (wipes the previous build first)
make docs-check # compile the snippets, then build in strict mode — what CI runs
make clean-site # remove the generated site/ and .cache/
make check-site # report outdated website dependencies (dry run)
make upgrade-site # upgrade website dependencies, rewriting uv.lock
make site serves the site; make docs-check is the one that builds it, and it is what
you want before pushing.
Code examples are compiled, not pasted¶
No example on this site is typed into a Markdown file. Every one is a real source file in a
Gradle test source set, embedded at build time by pymdownx.snippets. ./gradlew
compileTestKotlin compileTestJava type-checks all of them against the real API on every CI
run, so an example cannot silently rot when the library changes.
They live in the module whose API they document:
etcd-recipes-core/src/test/kotlin/website/ Kotlin
etcd-recipes-core/src/test/java/website/ Java
etcd-recipes-<satellite>/src/test/…/website/<module>/
Why the satellites need their own
etcd-recipes-core's test source set cannot see the satellite modules — they depend on
core, not the reverse. So Spring, Ktor, Jackson and Micrometer examples live in their
own module's src/test, and zensical.toml lists each one in base_path.
Adding an example¶
Write a plain function that is never called, and mark the interesting part:
fun basicMutex(client: Client) {
DistributedMutex(client, "/locks/orders").use { mutex ->
mutex.withLock { /* ... */ }
}
}
Then embed it:
=== "Kotlin"
```kotlin
DistributedMutex(client, "/locks/orders").use { mutex ->
mutex.withLock {
// Only one client across the cluster runs this at a time.
logger.info { "Critical section" }
}
}
```
=== "Java"
```java
try (DistributedMutex mutex = new DistributedMutex(client, "/locks/orders")) {
mutex.lock();
try {
// Only one client across the cluster runs this at a time.
System.out.println("Critical section");
} finally {
mutex.unlock();
}
}
```
Run make docs-check and you are done.
Rules¶
- They are not tests. No Kotest specs, no assertions, no
main(). Plain functions, never invoked. They exist to be compiled. - Section names must match
[a-z][-_0-9a-z]*— lowercase, digits,-,_.try-lockworks;tryLockwill not match, and the section silently will not render. - kotlinter and detekt lint them like any other source. Keep them small.
- Prefer showing both languages. Coroutines are the honest exception.
- A dangling snippet reference fails the build (
check_paths = true) rather than rendering an empty block. That is deliberate — please keep it that way.
zensical must run from website/etcd-recipes
pymdownx.snippets resolves base_path against the process working directory, not
against zensical.toml's location, so zensical build -f website/etcd-recipes/zensical.toml
from the repo root finds no snippets. Every make target cds first.
Versioning¶
The library version lives in gradle.properties; Kotlin and dependency versions in
gradle/libs.versions.toml. All modules share the one version.
Releasing bumps gradle.properties, moves the CHANGELOG.md [Unreleased] section under
the new heading, adds a RELEASE_NOTES.md entry, and updates the coordinates quoted in the
docs — the README download snippets plus several pages on this site. They are all literal
version strings, so:
is the reliable way to catch them; do not rely on the list of files staying accurate.
CI¶
.github/workflows/ci.yml runs detekt, then check koverXmlReport -PuseTestcontainers.
check and koverXmlReport must stay in one Gradle invocation
Splitting them changes the -PuseTestcontainers input, which invalidates the test
cache and makes the rerun hang against localhost:2379. The comments in ci.yml
record this and the codecov report-path quirk — read them before editing.
.github/workflows/docs.yml builds the site on every PR and deploys from master.