NAME

Net::Async::Kubernetes::Watcher - Auto-reconnecting Kubernetes watch as IO::Async::Notifier

VERSION

version 0.009

SYNOPSIS

my $watcher = $kube->watcher('Pod',
    namespace      => 'default',
    label_selector => 'app=web',
    on_added       => sub { my ($pod) = @_; say "Added: " . $pod->metadata->name },
    on_modified    => sub { my ($pod) = @_; say "Modified: " . $pod->metadata->name },
    on_deleted     => sub { my ($pod) = @_; say "Deleted: " . $pod->metadata->name },
    on_error       => sub { my ($status) = @_; warn "Error: $status->{message}" },
);

# Client-side filtering by name and event type
$kube->watcher('Pod',
    namespace   => 'default',
    names       => [qr/^nginx/, qr/^redis/],  # only matching names
    event_types => ['ADDED', 'DELETED'],        # skip MODIFIED
    on_added    => sub { ... },
    on_deleted  => sub { ... },
);

# Watch multiple resources concurrently
$kube->watcher('Deployment', namespace => 'production', on_modified => sub { ... });
$kube->watcher('Service', namespace => 'production', on_added => sub { ... });

# Stop watching
$watcher->stop;

# Restart
$watcher->start;

DESCRIPTION

An IO::Async::Notifier that watches a Kubernetes resource for changes. Created via "watcher" in Net::Async::Kubernetes.

The watcher automatically:

  • Reconnects when the server-side timeout expires

  • Resumes from the last resourceVersion to avoid missing events

  • Handles 410 Gone by clearing the resourceVersion and restarting

  • Retries a failed watch attempt -- a transport error, a rejection such as 401 or 403, or a stream that closes at once without an event or right after an ERROR event -- with an exponential backoff (1s, 2s, 4s, ... up to 30s by default, see "reconnect_delay", each shortened at random by up to a fifth, see "reconnect_jitter"), reports every such failure to "on_error" or as a warning, and gives up after "max_retries" consecutive failures when a limit is set

  • Filters events client-side by name patterns (names) and event types (event_types)

configure

Internal IO::Async::Notifier configuration method. Handles initialization of kube, resource, namespace, timeout, label_selector, field_selector, names, event_types, reconnect_delay, max_reconnect_delay, reconnect_jitter, max_retries, min_watch_duration, and all event callbacks (on_added, on_modified, on_deleted, on_error, on_event). Croaks on a reconnect_delay, max_reconnect_delay or min_watch_duration that is not a non-negative number, on a reconnect_jitter that is not a number from 0 to 1, and on a max_retries that is neither a non-negative integer nor undef.

kube

Returns the parent Net::Async::Kubernetes instance.

resource

Required. The Kubernetes resource kind to watch (e.g., 'Pod', 'Deployment'), or a qualified 'group/version/Kind' name to watch a specific API version -- see "expand_class" in Net::Async::Kubernetes.

namespace

Optional. Namespace to watch. Omit for cluster-scoped resources or to watch all namespaces.

timeout

Server-side timeout per watch cycle in seconds. Default: 300.

reconnect_delay

Seconds to wait before reconnecting after a failed watch attempt (see "on_error" for what counts as one). Default: 1. Each further consecutive failure doubles the delay, up to "max_reconnect_delay", and every delay is shortened at random by up to "reconnect_jitter". A reconnect that gets through -- an event arrives on the stream (an ERROR event does not count), or the watch cycle ends cleanly -- starts the next failure at reconnect_delay again. A watch cycle that ends cleanly (the server-side "timeout") is not a failure and reconnects at once; one that closes within "min_watch_duration" without an event, or right after an ERROR event, did not end cleanly.

max_reconnect_delay

Upper bound in seconds for the reconnect delay. Default: 30, the same cap client-go's reflector puts on its watch backoff: a cluster that comes back is noticed within half a minute, and one that stays away costs one request and one report per half minute.

reconnect_jitter

The share, from 0 to 1, by which each reconnect delay is shortened at random. Default: 0.2 -- a delay of 4 seconds becomes one between 3.2 and 4 seconds, rounded to the millisecond. Without it, watchers that fail together (every watch of a process, or of a fleet, on an API server that restarts) would send their reconnects together again on every step of the backoff.

The delay is only ever shortened: "reconnect_delay" and "max_reconnect_delay" stay upper bounds, and retryAfterSeconds in the report (see "on_error") is the delay the watcher actually waits. With the default each step of the backoff still lies above the one before it (0.8 times double the delay is more than the delay). 0 turns jitter off, for delays that are exactly the documented ones.

max_retries

How many consecutive failed watch requests are retried before the watcher gives up. Default: undef, retry for as long as the watcher runs. When the limit is exceeded the watcher stops and reports that it gave up (see "on_error"); start() begins again with the full count. 0 gives up on the first failure.

min_watch_duration

Seconds a watch stream has to stay open before it may end without an event. Default: 1. A stream that closes sooner without delivering a single event is no watch cycle that ran its course but a failed attempt (see "on_error"): reconnecting at once would send the API server one watch request after another in a tight loop -- a proxy or API server that accepts the watch and closes it straight away. The default is the threshold client-go's reflector uses for the same case (a "very short watch"): a real watch cycle runs until the server-side "timeout", minutes, so one second tells the two apart with room for a slow connection. 0 turns the check off.

label_selector

Optional label selector (e.g., 'app=web,env=prod').

field_selector

Optional field selector (e.g., 'status.phase=Running').

names

Optional client-side filter for resource names. Accepts a single regex, a string (exact match), or an arrayref of regexes/strings. Events whose resource name does not match any of the patterns are silently dropped before callbacks fire.

# Single regex
names => qr/^nginx/

# Multiple patterns (any match passes)
names => [qr/^nginx/, qr/^redis/]

# Exact string match
names => 'my-pod'

event_types

Optional client-side filter for event types. Accepts an arrayref of type strings (ADDED, MODIFIED, DELETED, ERROR). Events whose type is not in the list are silently dropped.

# Only ADDED and DELETED events
event_types => ['ADDED', 'DELETED']

When not set, event types are automatically derived from which callbacks are registered. If only on_added is set, only ADDED events are dispatched. If on_event is set (catch-all), all types pass through.

on_added

Callback for ADDED events. Receives the inflated IO::K8s object.

on_modified

Callback for MODIFIED events. Receives the inflated IO::K8s object.

on_deleted

Callback for DELETED events. Receives the inflated IO::K8s object.

on_error

Callback for errors of the watch. Receives a hashref shaped like a Kubernetes Status, in two cases:

  • An ERROR event on the stream, for example a 403 arriving mid-stream: the raw Status hashref the API server sent. 410 Gone is handled internally and never reaches it.

  • A failed watch attempt: the request fails outright (TLS support missing, a TLS or connection error, an unreachable API server), the API server rejects it with an HTTP error status (401, 403, 5xx), or the stream it opens ends badly -- within "min_watch_duration" without a single event, or right after an ERROR event other than 410 Gone (that event is reported first, as above). The watcher builds this Status itself. reason is WatchFailed, code the HTTP status of a rejected request, the code of the ERROR event the stream ended on, or 0 when neither is there (no response arrived, or the stream closed without an event), message names the resource, what the watcher does next and the cause (for a rejected request its HTTP status, reason and message, as in HTTP 403 Forbidden: pods is forbidden: ...), and details carries kind (the watched resource) and, while the watcher retries, retryAfterSeconds:

    {
        kind       => 'Status',
        apiVersion => 'v1',
        status     => 'Failure',
        reason     => 'WatchFailed',
        code       => 0,
        message    => 'watch Pod failed, retrying in 4s: Connection refused',
        details    => { kind => 'Pod', retryAfterSeconds => 4 },
    }

    Once "max_retries" is exceeded, the message says giving up after N retries instead and retryAfterSeconds is absent; the watcher has stopped by then.

A failed watch attempt is reported whatever "event_types" says. Without an on_error it is passed to warn instead, so a watch that cannot reach its cluster never fails silently. ERROR events without an on_error are dropped, as before. The callback may call stop(), which also cancels the reconnect just announced.

on_event

Catch-all callback. Receives the Kubernetes::REST::WatchEvent object. Called in addition to the type-specific callbacks.

start

Start (or restart) the watch stream. Called automatically when the watcher is added to the event loop. Safe to call multiple times (idempotent): a watcher waiting to reconnect after a failed request is already running. After stop(), or after giving up on "max_retries", it starts over with the full retry count.

stop

Stop the watch stream and cancel the current HTTP request, or a reconnect that is waiting out its delay. The watcher will not automatically reconnect until start() is called again.

SEE ALSO

Net::Async::Kubernetes, Kubernetes::REST::WatchEvent, IO::Async::Notifier

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-net-async-kubernetes/issues.

IRC

Join #kubernetes on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

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