NAME

EV::Etcd - Async etcd v3 client using native gRPC and EV/libev

SYNOPSIS

use v5.10;
use EV;
use EV::Etcd;

my $client = EV::Etcd->new(
    endpoints => ['127.0.0.1:2379'],
);

# Async put
$client->put('/my/key', 'value', sub {
    my ($resp, $err) = @_;
    die $err->{message} if $err;
    say "Put succeeded, revision: $resp->{header}{revision}";
});

# Async get
$client->get('/my/key', sub {
    my ($resp, $err) = @_;
    die $err->{message} if $err;
    say "Value: $resp->{kvs}[0]{value}";
});

# Watch
$client->watch('/my/key', sub {
    my ($resp, $err) = @_;
    return warn "Watch error: $err->{message}\n" if $err;
    for my $event (@{$resp->{events}}) {
        say "Event: $event->{type} on $event->{kv}{key}";
    }
});

EV::run;

DESCRIPTION

EV::Etcd provides a high-performance async client for etcd v3 using native gRPC Core C API integrated with the EV event loop.

METHODS

new

my $client = EV::Etcd->new(%options);

Options:

endpoints

ArrayRef of etcd endpoints (host:port). Optional; defaults to ['127.0.0.1:2379']. When more than one is provided, the client uses the first endpoint and rotates to subsequent endpoints on connection failure.

timeout

RPC timeout in seconds. Default is 30 seconds. Minimum value is 1 second.

max_retries

Maximum number of reconnection attempts for streaming operations (watch, lease_keepalive, election_observe) after a connection failure. Default is 3. Set to 0 to disable automatic reconnection.

health_interval

Interval in seconds for health monitoring. Default is 0 (disabled). When enabled, the client periodically checks the gRPC channel connectivity state and calls the on_health_change callback when the connection state changes.

on_health_change

Callback called when the connection health status changes. Receives two arguments: a boolean indicating health status (1=healthy, 0=unhealthy) and the current endpoint string.

my $client = EV::Etcd->new(
    endpoints => ['127.0.0.1:2379'],
    health_interval => 5,
    on_health_change => sub {
        my ($is_healthy, $endpoint) = @_;
        warn $is_healthy ? "Connected to $endpoint" : "Disconnected from $endpoint";
    },
);
auth_token

Pre-set authentication token. Use this to create an authenticated client without calling authenticate() first. Useful when you already have a valid token from a previous session.

my $client = EV::Etcd->new(
    endpoints => ['127.0.0.1:2379'],
    auth_token => $saved_token,
);

ENCODING

Keys and values are stored by etcd as raw bytes; this module does not perform any character encoding. If you pass a Perl string with the UTF-8 flag set (e.g. a literal containing non-ASCII characters under use utf8), the UTF-8 byte representation is what gets stored. Values returned by get are byte strings without the UTF-8 flag — string-equality with the original literal will fail unless you decode explicitly.

For character data, encode/decode at the boundary using Encode:

use Encode qw(encode_utf8 decode_utf8);

$client->put($key, encode_utf8($value), sub { ... });
$client->get($key, sub {
    my ($resp) = @_;
    my $value = decode_utf8($resp->{kvs}[0]{value});
});

ERROR HANDLING

Errors are returned as hash references with the following structure:

{
    code      => 14,              # gRPC status code (integer)
    status    => "UNAVAILABLE",   # gRPC status name (string)
    message   => "Connection refused",  # Error message
    source    => "get",           # Which operation failed
    retryable => 1,               # Whether the error is retryable
}

The retryable field indicates whether the error is transient (status codes: UNAVAILABLE, RESOURCE_EXHAUSTED, ABORTED, DEADLINE_EXCEEDED). Streaming operations (watch, keepalive, observe) automatically reconnect on transient failures according to the max_retries configuration. Unary RPCs (get, put, delete, etc.) do not retry automatically; use the retryable field to implement application-level retry logic.

KEY-VALUE OPERATIONS

put

$client->put($key, $value, $callback);
$client->put($key, $value, \%opts, $callback);

Put a key-value pair into etcd.

Options:

lease

Lease ID to associate with the key.

prev_kv

If true, returns the previous key-value pair in the response.

ignore_value

If true, updates the lease without changing the value.

ignore_lease

If true, updates the value without changing the lease.

The callback receives ($response, $error). Response keys: header, plus prev_kv (a kv hashref) when the prev_kv option is set.

get

$client->get($key, $callback);
$client->get($key, \%opts, $callback);

Get key(s) from etcd.

Options:

prefix

If true, returns all keys with the given prefix.

range_end

End of the key range to query.

limit

Maximum number of keys to return.

revision

Get keys at a specific revision.

keys_only

If true, returns only keys without values.

count_only

If true, returns only the count of keys.

sort_order

Sort order: "ascend" or "descend".

sort_target

What to sort by: "key", "version", "create", "mod", or "value".

serializable

If true, use serializable (faster but possibly stale) reads.

min_mod_revision, max_mod_revision

Filter keys by modification revision.

min_create_revision, max_create_revision

Filter keys by creation revision.

The callback receives ($response, $error). Response keys:

kvs

Array reference of key-value hashrefs (key, value, create_revision, mod_revision, version, lease).

count

Total number of keys matched (may exceed scalar @{$resp-{kvs}}> when limit is in effect).

more

True if more keys exist beyond the returned kvs (limit truncated the result).

Cluster response header (see "ERROR HANDLING").

delete

$client->delete($key, $callback);
$client->delete($key, \%opts, $callback);

Delete key(s) from etcd.

Options:

prefix

If true, deletes all keys with the given prefix.

range_end

End of the key range to delete.

prev_kv

If true, returns the deleted key-value pairs in the response.

The callback receives ($response, $error). Response keys: header, deleted (count of keys deleted), plus prev_kvs (array of kv hashrefs) when the prev_kv option is set.

WATCH SERVICE

watch

my $watch = $client->watch($key, $callback);
my $watch = $client->watch($key, \%opts, $callback);

Create a watch on a key or key range. Returns an EV::Etcd::Watch object.

The callback is invoked with ($response, $error) for each watch message. Response keys:

events

Array reference of event hashrefs (type = "PUT" or "DELETE", kv, and optionally prev_kv when the prev_kv option is set).

watch_id

Server-assigned watch identifier. Stable for the lifetime of the watch (including across auto-reconnect).

created

True on the first message after the watch is established.

header

Cluster response header.

Server-side cancellation (including compaction-induced) is delivered through the error callback path, not as a success response — $err-{source}> will be "watch" and $err-{message}> will contain the server's reason (typically including the compact revision when relevant).

Options:

prefix

If true, watches all keys with the given prefix.

range_end

End of the key range to watch.

start_revision

Revision to start watching from. If not specified, watches from the current revision. Use this to resume watching after a reconnect.

prev_kv

If true, the response will include the previous key-value pair for UPDATE and DELETE events.

progress_notify

If true, the server will periodically send progress notifications even when there are no events, allowing the client to know the current revision.

watch_id

Optional explicit watch ID. If not specified, the server assigns one.

auto_reconnect

If true, the watch will automatically reconnect after a connection failure, resuming from the last seen revision. Default is true. This is useful for long-running watches that should survive network interruptions.

my $watch = $client->watch('/my/key', {
    auto_reconnect => 1,
    progress_notify => 1,  # Helps track revision even during inactivity
}, sub {
    my ($event, $err) = @_;
    if ($err) {
        warn "Watch error: $err->{message}";
        return;
    }
    # Process events...
});

EV::Etcd::Watch Methods

cancel

$watch->cancel($callback);

Cancel the watch. The callback receives ($response, $error) when cancellation is complete. The response is an empty hash reference on success.

Calling cancel on an already-cancelled handle is safe: the callback fires immediately with success. The handle remains valid as a Perl reference until you drop it.

$watch->cancel(sub {
    my ($resp, $err) = @_;
    if ($err) {
        warn "Cancel failed: $err->{message}";
    } else {
        print "Watch cancelled\n";
    }
});

LEASE SERVICE

lease_grant

$client->lease_grant($ttl, $callback);

Grant a lease with the specified TTL (time-to-live) in seconds.

The callback receives ($response, $error). Response keys:

id

The lease ID.

ttl

The actual TTL granted by the server.

header

Standard response header.

lease_revoke

$client->lease_revoke($lease_id, $callback);

Revoke a lease. All keys attached to the lease will be deleted. The response contains header only.

lease_keepalive

my $keepalive = $client->lease_keepalive($lease_id, $callback);
my $keepalive = $client->lease_keepalive($lease_id, \%opts, $callback);

Keep a lease alive. Creates a bidirectional streaming connection that keeps the lease refreshed. Returns an EV::Etcd::Keepalive object that can be used to cancel the keepalive stream:

$keepalive->cancel(sub { my ($resp, $err) = @_; });

Options:

auto_reconnect

If true, the keepalive stream will automatically reconnect after a connection failure, with exponential backoff up to max_retries (set on the client). Default is 1 (enabled). Pass 0 to disable.

The keepalive callback receives ($response, $error) for each tick. On success the response includes id, ttl, and header. When the lease has expired the server sends ttl=0; the client maps that to an error callback with source => "keepalive" and status NOT_FOUND.

EV::Etcd::Keepalive Methods

cancel

$keepalive->cancel($callback);

Cancel the keepalive stream. The callback receives ($response, $error) when cancellation is complete. The response is an empty hash reference on success.

Calling cancel on an already-cancelled handle is safe: the callback fires immediately with success. The handle remains valid as a Perl reference until you drop it.

lease_time_to_live

$client->lease_time_to_live($lease_id, $callback);
$client->lease_time_to_live($lease_id, \%opts, $callback);

Get the remaining TTL of a lease.

Options:

keys

If true, also return the list of keys attached to this lease.

The callback receives ($response, $error). Response keys:

id

The lease ID.

ttl

Remaining TTL in seconds. Returns -1 if the lease has expired.

granted_ttl

The original TTL granted when the lease was created.

keys

Array of keys attached to this lease (only when the keys option is true).

header

Standard response header.

lease_leases

$client->lease_leases($callback);

List all active leases. The callback receives ($response, $error). Response keys: header, and leases — an array of hashrefs each with an id key.

LOCK SERVICE

EV::Etcd provides distributed locking through the etcd Lock service. Locks are tied to leases - when the lease expires or is revoked, the lock is automatically released.

lock

$client->lock($name, $lease_id, $callback);

Acquire a distributed lock.

Arguments:

name

The name (identifier) of the lock to acquire. This is a byte string that identifies the resource being locked. Multiple clients attempting to lock the same name will block until the lock is available.

lease_id

The ID of a lease to attach to the lock. The lock will be held for the duration of the lease. If the lease expires or is revoked, the lock is automatically released. You must first create a lease with lease_grant and optionally keep it alive with lease_keepalive.

callback

Called with ($response, $error) when the lock is acquired (or fails).

The response contains:

key

The key that holds the lock. This key is used to unlock the lock and should be stored by the caller. The key is unique to this lock holder and contains the lock name as a prefix.

header

Standard response header with cluster_id, member_id, revision, and raft_term.

Example:

# First, create a lease for the lock
$client->lease_grant(30, sub {
    my ($lease_resp, $err) = @_;
    die $err->{message} if $err;

    my $lease_id = $lease_resp->{id};

    # Now acquire the lock
    $client->lock("my-resource", $lease_id, sub {
        my ($lock_resp, $err) = @_;
        die $err->{message} if $err;

        my $lock_key = $lock_resp->{key};
        print "Lock acquired with key: $lock_key\n";

        # ... do protected work ...

        # Release the lock when done
        $client->unlock($lock_key, sub {
            my ($unlock_resp, $err) = @_;
            warn "Unlock failed: $err->{message}" if $err;
        });
    });
});

unlock

$client->unlock($key, $callback);

Release a distributed lock.

Arguments:

key

The lock key returned from a successful lock call. This is the unique key that was created to hold the lock ownership.

callback

Called with ($response, $error) when the unlock completes.

The response contains:

header

Standard response header with cluster_id, member_id, revision, and raft_term.

Note: You can also release a lock by revoking its associated lease with lease_revoke. This is useful if you want to release all resources associated with a lease at once.

AUTHENTICATION SERVICE

EV::Etcd provides full support for etcd's authentication and authorization system. Authentication uses username/password credentials, and authorization is based on roles with key-range permissions.

authenticate

$client->authenticate($username, $password, $callback);

Authenticate with etcd using username and password. On success, the client automatically stores the auth token and uses it for subsequent requests.

Arguments:

username

The username to authenticate as.

password

The password for the user.

callback

Called with ($response, $error) when authentication completes.

The response contains:

token

The authentication token (also automatically stored in the client).

header

Standard response header.

Example:

$client->authenticate('admin', 'secret', sub {
    my ($resp, $err) = @_;
    if ($err) {
        die "Authentication failed: $err->{message}";
    }
    say "Authenticated successfully";
    # Client now automatically uses the token for all requests
});

auth_enable

$client->auth_enable($callback);

Enable authentication on the etcd cluster.

Warning: Before enabling auth, you must create at least one user with the root role, otherwise you will be locked out of the cluster.

Example:

# First create root user
$client->user_add('root', 'rootpassword', sub {
    my ($resp, $err) = @_;
    die $err->{message} if $err;

    $client->user_grant_role('root', 'root', sub {
        my ($resp, $err) = @_;
        die $err->{message} if $err;

        # Now safe to enable auth
        $client->auth_enable(sub {
            my ($resp, $err) = @_;
            say "Authentication enabled" unless $err;
        });
    });
});

auth_disable

$client->auth_disable($callback);

Disable authentication on the etcd cluster. Requires root privileges.

auth_status

$client->auth_status($callback);

Check whether authentication is enabled on the etcd cluster. Response keys:

enabled

Boolean indicating whether authentication is enabled.

auth_revision

The current revision of the auth store.

header

Standard response header.

Example:

$client->auth_status(sub {
    my ($resp, $err) = @_;
    if ($resp->{enabled}) {
        say "Authentication is enabled (revision: $resp->{auth_revision})";
    } else {
        say "Authentication is disabled";
    }
});

User Management

user_add

$client->user_add($username, $password, $callback);

Create a new user.

Arguments:

username

The username for the new user.

password

The password for the new user.

callback

Called with ($response, $error) when complete.

user_delete

$client->user_delete($username, $callback);

Delete a user.

user_change_password

$client->user_change_password($username, $password, $callback);

Change a user's password.

user_get

$client->user_get($username, $callback);

Get information about a user.

The response contains:

roles

Array of role names assigned to the user.

Example:

$client->user_get('myuser', sub {
    my ($resp, $err) = @_;
    say "User has roles: @{$resp->{roles}}";
});

user_list

$client->user_list($callback);

List all users.

The response contains:

users

Array of usernames.

user_grant_role

$client->user_grant_role($username, $role_name, $callback);

Grant a role to a user.

Example:

$client->user_grant_role('myuser', 'readwrite', sub {
    my ($resp, $err) = @_;
    say "Role granted" unless $err;
});

user_revoke_role

$client->user_revoke_role($username, $role_name, $callback);

Revoke a role from a user.

Role Management

role_add

$client->role_add($role_name, $callback);

Create a new role.

Example:

$client->role_add('readonly', sub {
    my ($resp, $err) = @_;
    say "Role created" unless $err;
});

role_delete

$client->role_delete($role_name, $callback);

Delete a role.

role_get

$client->role_get($role_name, $callback);

Get information about a role, including its permissions.

The response contains:

perm

Array of permission objects, each containing:

perm_type

Permission type: "READ", "WRITE", or "READWRITE".

key

The key or key prefix this permission applies to.

range_end

The end of the key range (if applicable).

Example:

$client->role_get('myrole', sub {
    my ($resp, $err) = @_;
    for my $perm (@{$resp->{perm}}) {
        say "Permission: $perm->{perm_type} on $perm->{key}";
    }
});

role_list

$client->role_list($callback);

List all roles.

The response contains:

roles

Array of role names.

role_grant_permission

$client->role_grant_permission($role_name, $perm_type, $key, $range_end, $callback);

Grant a permission to a role.

Arguments:

role_name

The role to grant the permission to.

perm_type

The permission type: "READ", "WRITE", or "READWRITE".

key

The key or key prefix to grant access to.

range_end

The end of the key range. Use undef for a single key, or use the special value "\x00" after the last byte of the prefix to match all keys with that prefix.

callback

Called with ($response, $error) when complete.

Example:

# Grant read access to a single key
$client->role_grant_permission('readonly', 'READ', '/config/setting', undef, sub {
    my ($resp, $err) = @_;
    say "Permission granted" unless $err;
});

# Grant read/write access to all keys under /app/
# Range end is /app0 (the byte after / is 0)
$client->role_grant_permission('readwrite', 'READWRITE', '/app/', '/app0', sub {
    my ($resp, $err) = @_;
    say "Permission granted" unless $err;
});

role_revoke_permission

$client->role_revoke_permission($role_name, $key, $range_end, $callback);

Revoke a permission from a role.

Arguments:

role_name

The role to revoke the permission from.

key

The key or key prefix of the permission to revoke.

range_end

The end of the key range of the permission to revoke.

callback

Called with ($response, $error) when complete.

Complete Authentication Example

use EV;
use EV::Etcd;

my $client = EV::Etcd->new(endpoints => ['127.0.0.1:2379']);

# Setup authentication (run once, as root)
sub setup_auth {
    # Create a role with permissions
    $client->role_add('app-role', sub {
        my ($resp, $err) = @_;

        # Grant read/write on /app/ prefix
        $client->role_grant_permission('app-role', 'READWRITE', '/app/', '/app0', sub {
            my ($resp, $err) = @_;

            # Create user
            $client->user_add('appuser', 'apppassword', sub {
                my ($resp, $err) = @_;

                # Assign role to user
                $client->user_grant_role('appuser', 'app-role', sub {
                    my ($resp, $err) = @_;
                    say "Auth setup complete";
                    EV::break;
                });
            });
        });
    });
}

# Normal usage with authentication
sub use_with_auth {
    $client->authenticate('appuser', 'apppassword', sub {
        my ($resp, $err) = @_;
        die "Auth failed: $err->{message}" if $err;

        # Now all operations use the auth token
        $client->put('/app/key', 'value', sub {
            my ($resp, $err) = @_;
            say "Put succeeded" unless $err;
            EV::break;
        });
    });
}

use_with_auth();
EV::run;

MAINTENANCE SERVICE

EV::Etcd provides access to etcd's maintenance operations for cluster administration and monitoring.

status

$client->status($callback);

Get the status of the etcd member this client is connected to. Useful for health checks and cluster monitoring. Response keys:

version

The etcd server version.

db_size

Backend database size in bytes.

db_size_in_use

Database size in use after compaction.

leader

Member ID of the cluster leader.

raft_index

Current raft index of this member.

raft_term

Current raft term of the cluster.

raft_applied_index

Raft applied index of this member.

is_learner

True if this member is a learner (non-voting).

errors

Array of error strings if this member has any. Absent when no errors.

header

Standard response header.

compact

$client->compact($revision, $callback);
$client->compact($revision, \%opts, $callback);

Compact the key-value store up to the given revision. All revisions older than $revision are discarded.

Warning: compaction is irreversible — historical reads of older revisions fail after the compact completes.

Options:

physical

If true, the RPC waits until the compaction is physically applied to the local backend (entries fully removed). Default is false (the call returns once the compaction is logically committed).

The response contains header only.

alarm

$client->alarm($action, $callback);
$client->alarm($action, \%opts, $callback);

Get, activate, or deactivate alarms on etcd cluster members.

Arguments:

action

The alarm action to perform. Must be one of:

GET

List all active alarms.

ACTIVATE

Activate an alarm on a member.

DEACTIVATE

Deactivate an alarm on a member.

callback

Called with ($response, $error) when the operation completes.

Options:

member_id

The member ID to operate on. Required for ACTIVATE/DEACTIVATE on a specific member. Use 0 for all members.

alarm

The alarm type. Can be "NOSPACE" (storage quota exceeded) or "CORRUPT" (data corruption detected). Default is "NONE" which means all alarms.

The response contains:

alarms

Array of alarm objects, each containing:

member_id

The member ID where the alarm is active.

alarm

The alarm type as an integer.

alarm_type

The alarm type as a string ("NONE", "NOSPACE", or "CORRUPT").

header

Standard response header.

Example:

# List all alarms
$client->alarm('GET', sub {
    my ($resp, $err) = @_;
    for my $alarm (@{$resp->{alarms}}) {
        warn "Alarm on member $alarm->{member_id}: $alarm->{alarm_type}";
    }
});

# Deactivate NOSPACE alarm on all members
$client->alarm('DEACTIVATE', { alarm => 'NOSPACE' }, sub {
    my ($resp, $err) = @_;
    warn "Alarm deactivated" unless $err;
});

defragment

$client->defragment($callback);

Defragment the storage backend on the etcd member this client is connected to. This reclaims storage space by removing deleted keys and compacted revisions.

Warning: Defragmentation is a blocking operation and may cause latency spikes. Run it during maintenance windows.

The response contains:

header

Standard response header.

Example:

$client->defragment(sub {
    my ($resp, $err) = @_;
    if ($err) {
        warn "Defragment failed: $err->{message}";
    } else {
        say "Defragmentation complete";
    }
});

hash_kv

$client->hash_kv($callback);
$client->hash_kv($revision, $callback);

Compute the hash of the KV store up to the given revision. Useful for verifying data consistency across cluster members.

Arguments:

revision (optional)

The revision to hash up to. If not specified, uses the current revision.

callback

Called with ($response, $error) when the operation completes.

The response contains:

hash

The hash value of the KV store.

compact_revision

The compaction revision of the KV store.

header

Standard response header.

Example:

$client->hash_kv(sub {
    my ($resp, $err) = @_;
    say "KV hash: $resp->{hash}";
    say "Compact revision: $resp->{compact_revision}";
});

move_leader

$client->move_leader($target_id, $callback);

Transfer leadership to another member. Only the current leader can transfer leadership.

Arguments:

target_id

The member ID of the new leader. Must be a voting member (not a learner).

callback

Called with ($response, $error) when the operation completes.

The response contains:

header

Standard response header.

Example:

# Get current cluster status to find member IDs
$client->member_list(sub {
    my ($resp, $err) = @_;
    my $target = $resp->{members}[1]{id};  # Pick a different member

    $client->move_leader($target, sub {
        my ($resp, $err) = @_;
        if ($err) {
            warn "Failed to move leader: $err->{message}";
        } else {
            say "Leadership transferred";
        }
    });
});

ELECTION SERVICE

EV::Etcd provides leader election support through the etcd Election service. Elections use leases to ensure that leadership is automatically released when a leader fails.

election_campaign

$client->election_campaign($name, $lease_id, $value, $callback);

Campaign for leadership of an election.

This call blocks until the caller is elected as leader. Once elected, the caller should periodically keep the lease alive to maintain leadership.

Arguments:

name

The name of the election to campaign in.

lease_id

The lease ID to use for the campaign. The leadership is held for the duration of this lease.

value

The value to set when elected. Other clients can read this value to identify the current leader.

callback

Called with ($response, $error) when elected (or on failure).

The response contains:

leader

A hash containing the leader key information:

name

The election name.

key

The key in etcd that holds the leadership (use for proclaim/resign).

rev

The creation revision of the leader key.

lease

The lease ID attached to the leadership.

header

Standard response header.

Example:

$client->lease_grant(30, sub {
    my ($resp, $err) = @_;
    my $lease_id = $resp->{id};

    $client->election_campaign("my-election", $lease_id, "leader-1", sub {
        my ($resp, $err) = @_;
        if ($err) {
            warn "Failed to become leader: $err->{message}";
            return;
        }
        say "Elected as leader!";
        my $leader = $resp->{leader};   # hashref — pass to proclaim/resign
    });
});

election_leader

$client->election_leader($name, $callback);

Get the current leader of an election.

Arguments:

name

The name of the election.

callback

Called with ($response, $error) when complete.

The response contains:

kv

The key-value pair of the current leader, containing the leader's value.

header

Standard response header.

Returns an error if there is no current leader.

Example:

$client->election_leader("my-election", sub {
    my ($resp, $err) = @_;
    if ($err) {
        warn "No leader: $err->{message}";
    } else {
        say "Current leader value: $resp->{kv}{value}";
    }
});

election_proclaim

$client->election_proclaim($leader, $value, $callback);

Update the leader's value. Only the current leader can proclaim. The response contains header only.

Arguments:

leader

The leader hashref returned in $resp->{leader} from election_campaign.

value

The new value to announce.

callback

Called with ($response, $error) when complete.

Example:

$client->election_proclaim($leader, "new-value", sub {
    my ($resp, $err) = @_;
    warn "Proclaim failed: $err->{message}" if $err;
});

election_resign

$client->election_resign($leader, $callback);

Voluntarily give up leadership. The response contains header only.

Arguments:

leader

The leader hashref returned in $resp->{leader} from election_campaign.

callback

Called with ($response, $error) when complete.

Example:

$client->election_resign($leader, sub {
    my ($resp, $err) = @_;
    say "Resigned from leadership" unless $err;
});

election_observe

my $observe = $client->election_observe($name, $callback);
my $observe = $client->election_observe($name, \%opts, $callback);

Observe leader changes for an election. This creates a streaming connection that receives notifications whenever the leader changes. Returns an EV::Etcd::Observe object that can be used to cancel the observe stream:

$observe->cancel(sub { my ($resp, $err) = @_; });

Arguments:

name

The name of the election to observe.

callback

Called with ($response, $error) for each leader change.

Options:

auto_reconnect

If true, automatically reconnect after connection failures. Default is true.

The response contains:

kv

The key-value pair of the current leader.

header

Standard response header.

Example:

my $observe = $client->election_observe("my-election", sub {
    my ($resp, $err) = @_;
    if ($err) {
        warn "Observe error: $err->{message}";
        return;
    }
    say "Leader changed: $resp->{kv}{value}";
});

EV::Etcd::Observe Methods

cancel

$observe->cancel($callback);

Cancel the observe stream. The callback receives ($response, $error) when cancellation is complete. The response is an empty hash reference on success.

Calling cancel on an already-cancelled handle is safe: the callback fires immediately with success. The handle remains valid as a Perl reference until you drop it.

CLUSTER SERVICE

EV::Etcd provides cluster membership management through the Cluster service.

member_list

$client->member_list($callback);
$client->member_list(\%opts, $callback);

List all members in the etcd cluster.

Options:

linearizable

If true, perform a linearizable (strongly consistent) read. Default is 0 (serializable read — faster but may be slightly stale).

The response contains:

members

Array of member objects, each containing:

id

The member ID.

name

The member name.

peer_urls

Array of peer URLs for cluster communication.

client_urls

Array of client URLs for client requests.

is_learner

Boolean indicating if the member is a learner.

header

Standard response header.

Example:

$client->member_list(sub {
    my ($resp, $err) = @_;
    for my $member (@{$resp->{members}}) {
        say "Member $member->{name} (ID: $member->{id})";
        say "  Client URLs: @{$member->{client_urls}}";
    }
});

member_add

$client->member_add(\@peer_urls, $callback);
$client->member_add(\@peer_urls, \%opts, $callback);

Add a new member to the cluster.

Arguments:

peer_urls

Array of peer URLs for the new member.

callback

Called with ($response, $error) when complete.

Options:

is_learner

If true, add the member as a non-voting learner.

member_remove

$client->member_remove($member_id, $callback);

Remove a member from the cluster. The response contains header and members (the cluster's remaining members after removal).

member_update

$client->member_update($member_id, \@peer_urls, $callback);

Update a member's peer URLs. The response contains header and members.

member_promote

$client->member_promote($member_id, $callback);

Promote a learner member to a voting member. The response contains header and members.

TRANSACTIONS

EV::Etcd supports atomic transactions with compare-and-swap semantics.

txn

$client->txn(
    compare => \@compare_ops,
    success => \@success_ops,
    failure => \@failure_ops,
    callback => $callback,
);

# Or positional form:
$client->txn(\@compare, \@success, \@failure, $callback);

Execute an atomic transaction. If all compare operations succeed, the success operations are executed; otherwise, the failure operations are executed.

Compare operations:

{ key => $key, target => 'value', value => $expected }
{ key => $key, target => 'version', version => $expected }
{ key => $key, target => 'create', create_revision => $expected }
{ key => $key, target => 'mod', mod_revision => $expected }
{ key => $key, target => 'lease', lease => $expected }

The optional result field controls the comparison operator (default: =):

result => '='    # EQUAL (default)
result => '!='   # NOT_EQUAL
result => '<'    # LESS
result => '>'    # GREATER

Note: Specify exactly one target field (value/version/create_revision/mod_revision/lease) per compare operation. If multiple are provided, the last one processed takes precedence.

Request operations (for success/failure):

{ request_put => { key => $key, value => $value } }
{ request_delete_range => { key => $key } }
{ request_range => { key => $key } }

Short aliases are also accepted:

{ put => { key => $key, value => $value } }
{ delete => { key => $key } }
{ range => { key => $key } }

Example:

$client->txn(
    compare => [
        { key => '/counter', target => 'value', value => '0' }
    ],
    success => [
        { request_put => { key => '/counter', value => '1' } }
    ],
    failure => [],
    callback => sub {
        my ($resp, $err) = @_;
        say $resp->{succeeded} ? "Incremented" : "Already changed";
    },
);

CAVEATS

Fork safety: EV::Etcd clients must not be used after fork(). The background gRPC thread does not survive into the child process. Inherited client objects will be safely cleaned up on destruction (with a warning), but cannot perform any operations. Create a new client in the child process if needed.

AUTHOR

Yegor Korablev (egor@cpan.org)

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.