etcd

package module
v0.0.0-...-5f4309f Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 10 Imported by: 0

README

go-ruby-etcd/etcd

etcd — go-ruby-etcd

Docs License Go Coverage

A pure-Go (no cgo), MRI-faithful reimplementation of the Ruby etcdv3 gem's client surface for etcd v3 — the ergonomics and result/error model of the gem's connection object (get / put / del / exists?, watch, lease_*, transaction, lock / unlock) layered over the official pure-Go etcd client.

It does not reimplement the etcd protocol. It consumes go.etcd.io/etcd/client/v3 as its transport and maps the gem's API onto it, so a static, CGO=0 binary talks to a real etcd cluster.

It is the etcd backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-redis and go-ruby-pg.

Transport is a host seam. A Client drives an injected transport whose method set is satisfied directly by *clientv3.Client (no adapter). This makes every method's request-building and response-mapping logic testable against a deterministic in-memory transport — no external etcd, no cgo — so the suite holds 100% coverage on every arch under qemu, mirroring the go-ruby-nats embedded-server-plus-deterministic-fallback split. A separate live suite drives a real in-process embedded etcd for round-trip validation on native lanes.

Features

  • KVGet (single, range via WithRange, prefix via WithPrefix, WithFromKey, WithLimit, WithRevision, WithSerializable, WithKeysOnly, WithCountOnly), Put (WithLease, WithPrevKV), Del, Exists.
  • WatchWatch (channel form) and WatchBlock (the gem's watch(key){ |events| } block form) over keys, prefixes and ranges, with PUT / DELETE events and previous values.
  • LeaseLeaseGrant, LeaseKeepAliveOnce, LeaseRevoke, LeaseTTL (with attached keys).
  • TransactionTransaction { If / Then / Else } mapping the gem's compare / success / failure lists, with Compare over Value / Version / CreateRevision / ModRevision / LeaseValue.
  • LockLock / Unlock, a lease-backed mutex implementing etcd's concurrency recipe (lowest creation-revision owns; waiters watch their predecessor).
  • MaintenanceMembers, Status.
  • Errors — an Etcdv3-style error tree (Error + one sentinel per gRPC status code) matchable with errors.Is.

Usage

c, err := etcd.New(etcd.Config{Endpoints: []string{"127.0.0.1:2379"}})
if err != nil {
	log.Fatal(err)
}
defer c.Close()

ctx := context.Background()
c.Put(ctx, "/service/a", "up", etcd.WithLease(lease.ID))
gr, _ := c.Get(ctx, "/service/", etcd.WithPrefix())
for _, kv := range gr.Kvs {
	fmt.Println(kv.Key, "=", kv.Value)
}

c.Transaction(ctx, func(t *etcd.Txn) {
	t.If(etcd.Compare(etcd.Value("/service/a"), "=", "up")).
		Then(etcd.OpPut("/service/a", "draining")).
		Else(etcd.OpGet("/service/a"))
})

Ruby mapping

etcdv3 gem go-ruby-etcd/etcd
Etcdv3.new(endpoints: ...) etcd.New(etcd.Config{Endpoints: ...})
conn.put('k', 'v') c.Put(ctx, "k", "v")
conn.get('k', range_end: 'l') c.Get(ctx, "k", etcd.WithRange("l"))
conn.del('k') / conn.exists?('k') c.Del(ctx, "k") / c.Exists(ctx, "k")
conn.watch('k') { |evs| ... } c.WatchBlock(ctx, fn, "k") / c.Watch(ctx, "k")
conn.lease_grant(10) c.LeaseGrant(ctx, 10)
conn.transaction { |t| ... } c.Transaction(ctx, func(t *etcd.Txn){ ... })
conn.lock('n', 10) / conn.unlock c.Lock(ctx, "n", 10) / c.Unlock(ctx, lk)

Tests & coverage

The default suite runs with -race and holds 100% statement coverage on all three host OSes and the six supported 64-bit architectures (amd64, arm64, riscv64, loong64, ppc64le and big-endian s390x), driving the full client logic against a deterministic in-memory transport with no external etcd:

go test -race -cover ./...

The live/ nested module validates real round-trip behaviour against an in-process embedded etcd (native-only; kept out of the main module so its large dependency tree never enters this go.mod):

cd live && go test ./...

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-ruby-etcd/etcd authors.

WebAssembly

Unlike the rest of the go-ruby family, this library does not target WebAssembly: its backing engine — go.etcd.io/bbolt (memory-mapped files + file locking) — relies on mmap and native filesystem syscalls that the js/wasm and wasip1/wasm sandboxes do not provide. It ships for the six 64-bit native/qemu arches only.

Documentation

Overview

Package etcd is a pure-Go (CGO=0), MRI-faithful reimplementation of the Ruby etcdv3 gem's client surface for etcd v3.

It does not reimplement the etcd protocol. It consumes the official pure-Go client go.etcd.io/etcd/client/v3 as its transport and layers the ergonomics and result/error model of the etcdv3 gem on top: Client.Get / Put / Del / Exists (single, range and prefix), Watch, lease grant / keep-alive / revoke / ttl, Transaction (compare / success / failure), and a lease-backed Lock / Unlock mirroring etcd's concurrency recipe. Maintenance (Members, Status) is exposed too.

Transport is a host seam

A Client drives an injected [transport] whose method set is satisfied directly by *clientv3.Client (no adapter). This makes every method's logic testable against a deterministic in-memory transport with no external etcd and no cgo, so the suite reaches 100% coverage on every arch under qemu, mirroring the go-ruby-nats embedded-server-plus-deterministic-fallback split. A separate live suite (build tag "live") drives an in-process embedded etcd for real round-trip validation on native lanes.

Ruby mapping

conn = Etcdv3.new(endpoints: 'http://127.0.0.1:2379')  =>  etcd.New(etcd.Config{Endpoints: ...})
conn.put('k', 'v')                                     =>  c.Put(ctx, "k", "v")
conn.get('k')                                          =>  c.Get(ctx, "k")
conn.get('k', range_end: 'l')                          =>  c.Get(ctx, "k", etcd.WithRange("l"))
conn.del('k')                                          =>  c.Del(ctx, "k")
conn.exists?('k')                                      =>  c.Exists(ctx, "k")
conn.watch('k') { |events| ... }                       =>  c.WatchBlock(ctx, fn, "k") / c.Watch(ctx, "k")
conn.lease_grant(10)                                   =>  c.LeaseGrant(ctx, 10)
conn.transaction { |t| ... }                            =>  c.Transaction(ctx, func(t *etcd.Txn){ ... })
conn.lock('n', 10) / conn.unlock(key)                   =>  c.Lock(ctx, "n", 10) / c.Unlock(ctx, lk)

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCanceled           = &Error{Code: codes.Canceled, Name: "Canceled"}
	ErrUnknown            = &Error{Code: codes.Unknown, Name: "Unknown"}
	ErrInvalidArgument    = &Error{Code: codes.InvalidArgument, Name: "InvalidArgument"}
	ErrDeadlineExceeded   = &Error{Code: codes.DeadlineExceeded, Name: "DeadlineExceeded"}
	ErrNotFound           = &Error{Code: codes.NotFound, Name: "NotFound"}
	ErrAlreadyExists      = &Error{Code: codes.AlreadyExists, Name: "AlreadyExists"}
	ErrPermissionDenied   = &Error{Code: codes.PermissionDenied, Name: "PermissionDenied"}
	ErrResourceExhausted  = &Error{Code: codes.ResourceExhausted, Name: "ResourceExhausted"}
	ErrFailedPrecondition = &Error{Code: codes.FailedPrecondition, Name: "FailedPrecondition"}
	ErrAborted            = &Error{Code: codes.Aborted, Name: "Aborted"}
	ErrOutOfRange         = &Error{Code: codes.OutOfRange, Name: "OutOfRange"}
	ErrUnimplemented      = &Error{Code: codes.Unimplemented, Name: "Unimplemented"}
	ErrInternal           = &Error{Code: codes.Internal, Name: "Internal"}
	ErrUnavailable        = &Error{Code: codes.Unavailable, Name: "Unavailable"}
	ErrDataLoss           = &Error{Code: codes.DataLoss, Name: "DataLoss"}
	ErrUnauthenticated    = &Error{Code: codes.Unauthenticated, Name: "Unauthenticated"}
)

The etcdv3 error tree: one sentinel per gRPC status code. Compare with errors.Is, e.g. errors.Is(err, etcd.ErrNotFound).

View Source
var ErrEmptyKey = &Error{Code: codes.InvalidArgument, Name: "InvalidArgument", Message: "empty key"}

ErrEmptyKey is returned by key operations when the key is empty; etcd rejects empty keys, and rejecting them locally avoids a needless round trip.

Functions

This section is empty.

Types

type Client

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

Client is an etcd v3 client bound to a transport seam. It mirrors the etcdv3 gem's connection object: it builds requests, drives them over the transport, and maps responses to the gem's result and error model. Construct one with New; the zero value is not usable.

func New

func New(cfg Config) (*Client, error)

New connects to etcd using cfg and returns a Client. It builds a *clientv3.Client, which dials lazily, so New returns without a round trip and fails only on a malformed configuration (for example no endpoints).

func (*Client) Close

func (c *Client) Close() error

Close releases the client's resources, mirroring the gem connection's close.

func (*Client) Del

func (c *Client) Del(ctx context.Context, key string, opts ...OpOption) (*DelResult, error)

Del deletes key, or a range/prefix when a range option is supplied. It mirrors the gem's #del.

func (*Client) Exists

func (c *Client) Exists(ctx context.Context, key string) (bool, error)

Exists reports whether key is present. It mirrors the gem's #exists? and uses a count-only read so it transfers no values.

func (*Client) Get

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

Get retrieves key, or a range/prefix of keys when a range option is supplied. It mirrors the gem's #get.

func (*Client) LeaseGrant

func (c *Client) LeaseGrant(ctx context.Context, ttl int64) (*LeaseGrantResult, error)

LeaseGrant grants a lease that expires after ttl seconds. It mirrors the gem's #lease_grant.

func (*Client) LeaseKeepAliveOnce

func (c *Client) LeaseKeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResult, error)

LeaseKeepAliveOnce renews a lease once. It mirrors the gem's #lease_keep_alive_once (the single-shot keep-alive).

func (*Client) LeaseRevoke

func (c *Client) LeaseRevoke(ctx context.Context, id LeaseID) (*LeaseRevokeResult, error)

LeaseRevoke revokes a lease, deleting every key attached to it. It mirrors the gem's #lease_revoke.

func (*Client) LeaseTTL

func (c *Client) LeaseTTL(ctx context.Context, id LeaseID, withKeys bool) (*LeaseTTLResult, error)

LeaseTTL returns a lease's remaining TTL. When withKeys is true the result also lists the keys attached to the lease. It mirrors the gem's #lease_ttl.

func (*Client) Lock

func (c *Client) Lock(ctx context.Context, name string, ttl int64) (*Lock, error)

Lock acquires a distributed lock named name, backed by a lease of ttl seconds, and blocks until it is held (or ctx is cancelled). It mirrors the etcdv3 gem's #lock and implements etcd's concurrency recipe: each caller writes a unique, lease-bound key under name/, and the caller whose key has the lowest creation revision owns the lock; later waiters watch the key just ahead of them until it is deleted.

func (*Client) Members

func (c *Client) Members(ctx context.Context) (*MembersResult, error)

Members lists the cluster's members. It mirrors the gem's #members.

func (*Client) Put

func (c *Client) Put(ctx context.Context, key, value string, opts ...OpOption) (*PutResult, error)

Put stores value at key. It mirrors the gem's #put and honours WithLease and WithPrevKV.

func (*Client) Status

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

Status returns the status of the member serving endpoint. It mirrors the gem's #status.

func (*Client) Transaction

func (c *Client) Transaction(ctx context.Context, build func(*Txn)) (*TxnResult, error)

Transaction runs an etcd transaction. The callback fills a Txn with comparisons (If) and success/failure operations (Then/Else); the transaction is then committed atomically. It mirrors the gem's #transaction block.

func (*Client) Unlock

func (c *Client) Unlock(ctx context.Context, lk *Lock) error

Unlock releases a held lock: it deletes the ownership key and revokes its lease. It mirrors the gem's #unlock.

func (*Client) Watch

func (c *Client) Watch(ctx context.Context, key string, opts ...OpOption) <-chan WatchResult

Watch watches key (or a prefix/range when a range option is supplied) and returns a channel of event batches. The channel closes when ctx is cancelled or the watch is otherwise terminated. It mirrors the gem's #watch.

func (*Client) WatchBlock

func (c *Client) WatchBlock(ctx context.Context, fn func([]Event), key string, opts ...OpOption) error

WatchBlock watches key and invokes fn with each non-empty batch of events, mirroring the etcdv3 gem's block form conn.watch(key) { |events| ... }. It blocks until the watch ends (ctx cancelled) or a terminal error arrives, returning that error (nil on a clean, ctx-driven stop).

type Cmp

type Cmp = clientv3.Cmp

Cmp is a transaction comparison, built with Compare over one of Value, Version, CreateRevision, ModRevision or LeaseValue. It aliases the clientv3 type so it drives the real transaction machinery directly.

func Compare

func Compare(cmp Cmp, op string, v any) Cmp

Compare finishes a comparison: op is one of "=", "!=", ">", "<" and v is the value to compare the target against. It mirrors the gem's comparison DSL.

func CreateRevision

func CreateRevision(key string) Cmp

CreateRevision targets a key's creation revision in a comparison.

func LeaseValue

func LeaseValue(key string) Cmp

LeaseValue targets a key's attached lease in a comparison.

func ModRevision

func ModRevision(key string) Cmp

ModRevision targets a key's modification revision in a comparison.

func Value

func Value(key string) Cmp

Value targets a key's value in a comparison (the gem's txn.value).

func Version

func Version(key string) Cmp

Version targets a key's version in a comparison.

type Config

type Config struct {
	// Endpoints is the list of etcd endpoints to connect to, e.g.
	// []string{"127.0.0.1:2379"}. A leading "http://" or "https://" scheme is
	// accepted and normalised away, matching the gem which takes a URL.
	Endpoints []string
	// DialTimeout bounds the initial connection. Zero uses the client default.
	DialTimeout time.Duration
	// Username and Password enable authenticated connections when both are set.
	Username string
	Password string
	// TLS, when set, is applied to the underlying client for https endpoints.
	// It is optional; nil means plaintext (or scheme-derived) transport.
	TLS *tls.Config
}

Config configures a Client. The fields mirror the keywords the etcdv3 gem's Etcdv3.new accepts that affect connecting to the cluster.

type Connection

type Connection = Client

Connection is the etcdv3 gem's name for the object Etcdv3.new returns. It is an alias for Client.

type DelResult

type DelResult struct {
	Header  Header
	Deleted int64
	PrevKvs []KeyValue
}

DelResult is the reply to Del: how many keys were deleted and, when requested, their previous values.

type Error

type Error struct {
	// Code is the gRPC status code etcd reported.
	Code codes.Code
	// Name is the etcdv3 error-class name for Code (e.g. "NotFound").
	Name string
	// Message is the human-readable detail from the status.
	Message string
	// contains filtered or unexported fields
}

Error is the base of the etcdv3 error tree. It carries the gRPC status code etcd returned and mirrors the etcdv3 gem, whose error classes map one-to-one onto the gRPC status codes. Match a specific kind with errors.Is against one of the exported sentinels (for example ErrNotFound); the match is by status code, so a wrapped Error compares equal to its sentinel.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether target is an *Error with the same status code, so the exported sentinels match any Error of the same kind regardless of message.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying transport error for errors.Unwrap.

type Event

type Event struct {
	Type   EventType
	Kv     KeyValue
	PrevKv *KeyValue
}

Event is a single watch event: the change kind and the affected key-value, with the previous value when the watch requested it.

type EventType

type EventType int

EventType is the kind of change a watch delivers.

const (
	// EventPut is a create or update.
	EventPut EventType = iota
	// EventDelete is a deletion (or lease expiry).
	EventDelete
)

func (EventType) String

func (t EventType) String() string

String renders the event type as the gem does ("PUT" / "DELETE").

type GetResult

type GetResult struct {
	Header Header
	Kvs    []KeyValue
	More   bool
	Count  int64
}

GetResult is the reply to Get: the matched key-values plus range metadata. It mirrors the gem's range response (kvs, count, more).

func (*GetResult) First

func (r *GetResult) First() *KeyValue

First returns the first key-value, or nil when the result is empty. It mirrors the common gem idiom of reading response.kvs.first.

type Header struct {
	ClusterID uint64
	MemberID  uint64
	Revision  int64
	RaftTerm  uint64
}

Header is the response header etcd returns with every reply.

type KeyValue

type KeyValue struct {
	Key            string
	Value          string
	CreateRevision int64
	ModRevision    int64
	Version        int64
	Lease          int64
}

KeyValue mirrors the gem's key-value object (etcd's mvccpb.KeyValue).

type Kvs

type Kvs = []KeyValue

Kvs is a convenience alias for a slice of KeyValue, echoing the gem's naming.

type LeaseGrantResult

type LeaseGrantResult struct {
	Header Header
	ID     LeaseID
	TTL    int64
	Error  string
}

LeaseGrantResult is the reply to LeaseGrant. ID is the granted lease id (the gem's lease['ID']); Error is set when the grant partially failed.

type LeaseID

type LeaseID = clientv3.LeaseID

LeaseID identifies a lease. It mirrors clientv3.LeaseID (an int64), which is the "ID" the etcdv3 gem returns from lease_grant.

type LeaseKeepAliveResult

type LeaseKeepAliveResult struct {
	Header Header
	ID     LeaseID
	TTL    int64
}

LeaseKeepAliveResult is the reply to a single keep-alive.

type LeaseRevokeResult

type LeaseRevokeResult struct {
	Header Header
}

LeaseRevokeResult is the reply to LeaseRevoke.

type LeaseTTLResult

type LeaseTTLResult struct {
	Header     Header
	ID         LeaseID
	TTL        int64
	GrantedTTL int64
	Keys       []string
}

LeaseTTLResult is the reply to LeaseTTL: the remaining and granted TTLs and, when requested, the keys attached to the lease.

type Lock

type Lock struct {
	// Key is the lock-ownership key this holder created.
	Key string
	// Lease is the lease attached to Key; revoking it also frees the lock.
	Lease LeaseID
	// contains filtered or unexported fields
}

Lock is a held distributed lock: the key that represents ownership and the lease that keeps it alive. Release it with Client.Unlock.

type Member

type Member struct {
	ID         uint64
	Name       string
	PeerURLs   []string
	ClientURLs []string
	IsLearner  bool
}

Member describes one cluster member, mirroring the gem's member object.

type MembersResult

type MembersResult struct {
	Header  Header
	Members []Member
}

MembersResult is the reply to Members.

type Op

type Op = clientv3.Op

Op is a transaction operation, built with OpGet, OpPut or OpDelete.

func OpDelete

func OpDelete(key string, opts ...OpOption) Op

OpDelete builds a delete operation for a transaction branch.

func OpGet

func OpGet(key string, opts ...OpOption) Op

OpGet builds a get operation for a transaction branch.

func OpPut

func OpPut(key, val string, opts ...OpOption) Op

OpPut builds a put operation for a transaction branch.

type OpOption

type OpOption = clientv3.OpOption

OpOption is a per-operation option (prefix, range, lease, ...). It aliases the clientv3 option type so options compose across this package and the underlying client.

func WithCountOnly

func WithCountOnly() OpOption

WithCountOnly asks a read to return only the count of matching keys.

func WithFromKey

func WithFromKey() OpOption

WithFromKey makes the operation act on all keys >= key.

func WithKeysOnly

func WithKeysOnly() OpOption

WithKeysOnly asks a read to return keys without their values.

func WithLease

func WithLease(id LeaseID) OpOption

WithLease attaches a lease to a Put; mirrors the gem's lease keyword.

func WithLimit

func WithLimit(n int64) OpOption

WithLimit caps the number of keys a range read returns.

func WithPrefix

func WithPrefix() OpOption

WithPrefix makes the operation act on every key sharing the given key as a prefix (the gem's range/prefix reads and deletes).

func WithPrevKV

func WithPrevKV() OpOption

WithPrevKV asks Put/Del to return the previous key-value; mirrors prev_kv.

func WithRange

func WithRange(end string) OpOption

WithRange makes the operation act on the half-open range [key, end); mirrors the gem's range_end keyword.

func WithRevision

func WithRevision(rev int64) OpOption

WithRevision reads keys as of the given store revision.

func WithSerializable

func WithSerializable() OpOption

WithSerializable permits a serializable (non-linearizable) read.

type PutResult

type PutResult struct {
	Header Header
	PrevKv *KeyValue
}

PutResult is the reply to Put; PrevKv is set when the previous value was requested (or surfaced by the backend).

type StatusResult

type StatusResult struct {
	Header    Header
	Version   string
	DbSize    int64
	Leader    uint64
	RaftIndex uint64
	RaftTerm  uint64
	IsLearner bool
}

StatusResult is the reply to Status: a member's version, database size and raft state. It mirrors the gem's #status.

type Txn

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

Txn accumulates a transaction's comparisons and branches. It is filled inside the callback passed to Client.Transaction and mirrors the gem's txn object whose compare / success / failure lists map to Txn.If / Txn.Then / Txn.Else.

func (*Txn) Else

func (t *Txn) Else(ops ...Op) *Txn

Else adds operations to the failure branch (the gem's txn.failure).

func (*Txn) If

func (t *Txn) If(cs ...Cmp) *Txn

If adds comparisons; the success branch runs only if they all hold. It mirrors setting the gem's txn.compare.

func (*Txn) Then

func (t *Txn) Then(ops ...Op) *Txn

Then adds operations to the success branch (the gem's txn.success).

type TxnOpResult

type TxnOpResult struct {
	Get *GetResult
	Put *PutResult
	Del *DelResult
}

TxnOpResult is one operation's reply within a transaction; exactly one of the fields is set, matching the operation kind.

type TxnResult

type TxnResult struct {
	Header    Header
	Succeeded bool
	Responses []TxnOpResult
}

TxnResult is the reply to a transaction: whether the comparisons held and the per-operation replies of the branch that ran.

type WatchResult

type WatchResult struct {
	Header          Header
	Events          []Event
	Canceled        bool
	Created         bool
	CompactRevision int64
	Err             error
}

WatchResult is one batch of watch events, mirroring a clientv3 WatchResponse. A batch with Err set is terminal.

Jump to

Keyboard shortcuts

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