etcd

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 6 Imported by: 0

README

etcd

Thin, option-configured wrapper around go.etcd.io/etcd/client/v3 v3.6.

Targets the dominant etcd use case in ad-tech/finance services — service registration (Put + Lease) and discovery (Get + Watch). A scan of 11 local Go projects using etcd showed KV + Lease at 100% adoption, Watch at 73%, and Lock/Mutex/Election at 0% — so this wrapper covers KV + Lease + Watch and deliberately omits the concurrency primitives (reach them via Client() if needed).

Why

etcd is the standard distributed coordination store: service registration/discovery, distributed config, leader-elected singletons. A thin, ergonomic wrapper with metrics + a fail-fast ping lowers the boilerplate every service re-implements.

Install

go get github.com/v8fg/kit4go/etcd

Isolated Go module — importing it pulls etcd's gRPC/protobuf/zap tree into your module, not the rest of kit4go.

Quick start

ctx := context.Background()
c, err := etcd.New(ctx, etcd.WithEndpoints("http://localhost:2379"))
if err != nil { log.Fatal(err) }
defer c.Close()

// Register with a lease: the key auto-expires if the process stops keep-aliving.
lease, _ := c.Grant(ctx, 30)
c.Put(ctx, "/services/bidder/inst-1", "10.0.0.1:8080", clientv3.WithLease(lease.ID))

// Discover.
resp, _ := c.Get(ctx, "/services/bidder/", clientv3.WithPrefix())
for _, kv := range resp.Kvs { /* kv.Key, kv.Value */ }

Operations

Method Notes
Put / Get / Delete KV; forward OpOptions (WithPrefix, WithLease, WithLimit, ...) untouched
Grant / KeepAlive / Revoke lease lifecycle; KeepAlive returns a channel the caller drains
Watch subscribe to key/prefix changes; returns etcd's WatchChan
Status cluster health (also used by the construction ping)

Txn, Compact, Cluster, Auth, and the concurrency package (Mutex/Lock/Election) are reached via Client()*clientv3.Client.

Construction

New opens the client and runs a Status ping (bounded by the context, 10s fallback) to fail fast on an unreachable cluster — gRPC dial can succeed against a dead peer, surfacing the failure only on the first op otherwise.

Options

WithEndpoints (required), WithDialTimeout (default 5s), WithDialKeepAliveTime, WithTLSConfig, WithUsername/WithPassword, WithAutoSyncInterval, WithRejectOldCluster.

Metrics & events

c.SetOnEvent(func(e etcd.Event) { /* e.Kind, e.Outcome */ })
m := c.Metrics() // Puts, Gets, Deletes, Grants, Watches, Errors

The hook runs on the calling goroutine; keep it cheap. When nil, the cost is a single atomic-pointer load per op (effectively zero overhead).

Mock seam

*clientv3.Client embeds the KV/Lease/Watcher/Maintenance interfaces, whose methods promote to the client. A local etcdAPI interface subset is therefore satisfied by structural promotion — the sole unit-test strategy (there is no miniredis-equivalent for etcd). Wrap(*clientv3.Client) adopts an existing client.

Testing

go test -short -race -cover ./...          # unit (mock), ~96% coverage
# integration (optional, needs a live cluster):
docker run -d -p 2379:2379 -e ALLOW_NONE_AUTHENTICATION=yes bitnami/etcd
ETCD_ENDPOINT=http://127.0.0.1:2379 go test -run Integration -v ./etcd/

Documentation

Overview

Package etcd is a thin, option-configured wrapper around go.etcd.io/etcd/client/v3.

It targets the dominant etcd use case in ad-tech/finance services — service registration (Put + Lease) and discovery (Get + Watch) — and provides ergonomic construction (functional options + sane defaults), a fail-fast cluster ping at construction, pass-through KV (Put/Get/Delete), Lease (Grant/KeepAlive/Revoke) and Watch operations, lightweight metrics + an event hook, an escape hatch to the underlying *clientv3.Client, and a graceful Close. The concurrency primitives (Mutex/Lock/Election) are deliberately NOT wrapped (0 local usage; reach them via Client() if needed) — this keeps the surface thin.

Index

Examples

Constants

View Source
const (
	KindPut       = "put"
	KindGet       = "get"
	KindDelete    = "delete"
	KindGrant     = "grant"
	KindKeepAlive = "keepalive"
	KindRevoke    = "revoke"
	KindWatch     = "watch"
	KindStatus    = "status"
)

Event kinds.

View Source
const (
	OutcomeSuccess = "success"
	OutcomeError   = "error"
)

Event outcomes.

Variables

View Source
var ErrNoEndpoints = errors.New("etcd: at least one endpoint required (WithEndpoints)")

ErrNoEndpoints is returned by New when no endpoints were configured.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client wraps an etcd client/v3 client. It is safe for concurrent use: all methods are goroutine-safe and Close is idempotent.

func New

func New(ctx context.Context, opts ...Option) (*Client, error)

New connects to the etcd cluster, verifies reachability with a Status call, and returns a Client.

The context bounds the construction-time ping. If it carries no deadline, a 10s fallback is applied. etcd's clientv3.New opens a gRPC connection but does not guarantee the peer is live, so the Status ping fail-fast on a dead node.

Example

ExampleNew shows the service-registration + discovery flow. It is a compile-checked illustration (an Example without an // Output: comment is compiled but not executed); wire your own endpoints to run it against a live etcd.

package main

import (
	"context"
	"log"

	clientv3 "go.etcd.io/etcd/client/v3"

	"github.com/v8fg/kit4go/etcd"
)

func main() {
	ctx := context.Background()
	c, err := etcd.New(ctx, etcd.WithEndpoints("http://localhost:2379"))
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()

	// Register a service instance with a lease: the key auto-expires if the
	// process stops keep-aliving (crash → key vanishes → discovery drops it).
	lease, err := c.Grant(ctx, 30) // 30s TTL
	if err != nil {
		log.Fatal(err)
	}
	if _, err := c.Put(ctx, "/services/bidder/inst-1", "10.0.0.1:8080", clientv3.WithLease(lease.ID)); err != nil {
		log.Fatal(err)
	}

	// Discover: list all bidder instances.
	resp, err := c.Get(ctx, "/services/bidder/", clientv3.WithPrefix())
	if err != nil {
		log.Fatal(err)
	}
	for _, kv := range resp.Kvs {
		_ = kv // key = instance path, value = address
	}
}

func Wrap

func Wrap(raw *clientv3.Client) *Client

Wrap adopts an existing *clientv3.Client. The Client does not own it: Close is a no-op. Useful for sharing a client.

func (*Client) Client

func (c *Client) Client() *clientv3.Client

Client returns the underlying *clientv3.Client for anything the wrapper does not expose directly (Txn, Compact, Cluster, Auth, concurrency.Mutex/Election). Returns nil when the Client was built from a mock (newWithAPI).

func (*Client) Close

func (c *Client) Close() error

Close releases the underlying gRPC connection. No-op for a wrapped or mock-injected client, and safe to call any number of times: clientv3.Client.Close is not itself idempotent (a second close returns "context canceled"), so for an owning client only the first call reaches the underlying close.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error)

Delete removes key(s). Forward OpOptions (WithPrefix, WithFromKey, ...) untouched.

func (*Client) Get

func (c *Client) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error)

Get reads key(s). Forward OpOptions (WithPrefix, WithRange, WithLimit, ...) untouched.

func (*Client) Grant

func (c *Client) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGrantResponse, error)

Grant creates a lease with the given TTL (seconds) and returns its ID. Pair with KeepAlive to keep it alive, or Put(..., WithLease(id)) to attach it.

func (*Client) KeepAlive

func (c *Client) KeepAlive(ctx context.Context, id clientv3.LeaseID) (<-chan *clientv3.LeaseKeepAliveResponse, error)

KeepAlive keeps a lease alive until ctx is cancelled, streaming responses on the returned channel. The caller MUST drain it (range over it) or the keep-alive goroutine stalls. The wrapper increments the counter once on start.

func (*Client) Metrics

func (c *Client) Metrics() Metrics

Metrics returns a snapshot of the counters.

func (*Client) Options

func (c *Client) Options() Options

Options returns the resolved options the client was built with.

The struct includes Password: do not log or serialize it verbatim.

func (*Client) Put

func (c *Client) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error)

Put stores a key/value. Forward OpOptions (WithLease, WithPrevKV, ...) untouched.

func (*Client) Revoke

Revoke revokes a lease immediately (deletes all keys attached to it).

func (*Client) SetOnEvent

func (c *Client) SetOnEvent(fn func(Event))

SetOnEvent installs a hook fired after each operation (nil disables it). The hook runs on the calling goroutine; keep it cheap and must not panic (a panic propagates to the caller — the wrapper does not recover). When nil, the cost is a single atomic-pointer load per operation (effectively zero overhead).

func (*Client) Status

func (c *Client) Status(ctx context.Context, endpoint string) (*clientv3.StatusResponse, error)

Status returns cluster status for the given endpoint (health, leader, version).

func (*Client) Watch

func (c *Client) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan

Watch subscribes to changes on key (or a prefix with WithPrefix). It returns etcd's WatchChan; range over it to receive events. The wrapper fires one event on subscription start. The channel is closed when ctx is cancelled.

type Event

type Event struct {
	Kind    string // KindPut, KindGet, KindDelete, KindGrant, KindKeepAlive, KindRevoke, KindWatch, KindStatus
	Outcome string // OutcomeSuccess or OutcomeError
}

Event is fired after each operation when an OnEvent hook is installed.

type Metrics

type Metrics struct {
	Puts    uint64 // Put calls
	Gets    uint64 // Get calls
	Deletes uint64 // Delete calls
	Grants  uint64 // Grant calls
	Watches uint64 // Watch subscriptions started
	Errors  uint64 // any operation returning a non-nil error
}

Metrics is a point-in-time snapshot of Client counters (all atomic loads).

type Option

type Option func(*Options)

Option configures Options.

func WithAutoSyncInterval

func WithAutoSyncInterval(d time.Duration) Option

WithAutoSyncInterval sets how often endpoints are re-resolved from the cluster (0 disables auto-sync).

func WithDialKeepAliveTime

func WithDialKeepAliveTime(d time.Duration) Option

WithDialKeepAliveTime sets the gRPC keepalive ping interval (0 -> client default 10s).

func WithDialTimeout

func WithDialTimeout(d time.Duration) Option

WithDialTimeout sets the connection dial timeout (0 -> client default 5s).

func WithEndpoints

func WithEndpoints(endpoints ...string) Option

WithEndpoints sets the etcd cluster URLs (e.g. "http://localhost:2379"). Copies the slice so later caller mutation cannot affect the client. Required.

func WithPassword

func WithPassword(p string) Option

WithPassword sets the auth password.

func WithRejectOldCluster

func WithRejectOldCluster(b bool) Option

WithRejectOldCluster rejects connecting to an incompatible (old) cluster.

func WithTLSConfig

func WithTLSConfig(c *tls.Config) Option

WithTLSConfig enables TLS.

func WithUsername

func WithUsername(u string) Option

WithUsername sets the auth username (pair with WithPassword).

type Options

type Options struct {
	Endpoints         []string
	DialTimeout       time.Duration
	DialKeepAliveTime time.Duration
	TLS               *tls.Config
	Username          string
	Password          string
	AutoSyncInterval  time.Duration
	RejectOldCluster  bool
}

Options configures the etcd client. Zero-valued tuning fields defer to client/v3's own defaults; only Endpoints is required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL