EV-Etcd
view release on metacpan or search on metacpan
eg/resumable_watch.pl view on Meta::CPAN
#!/usr/bin/env perl
#
# resumable_watch.pl - A watcher that survives process restarts without
# missing events. Persists the last seen revision to a file; on startup it
# watches from the revision after it, so etcd replays every change made
# while the watcher was down.
#
# Handles the compaction edge case: if the server has compacted that history
# away, the watch is cancelled with compact_revision set. We then list the
# current state of the prefix and continue from there, accepting the gap.
#
# Try it:
# $ perl eg/resumable_watch.pl /myapp/config/
# $ etcdctl put /myapp/config/foo bar
# $ ^C
# $ etcdctl put /myapp/config/baz qux # while we're down
# $ perl eg/resumable_watch.pl /myapp/config/ # picks up baz
#
use v5.10;
use strict;
use warnings;
use lib 'blib/lib', 'blib/arch';
use EV;
use EV::Etcd;
my $prefix = $ARGV[0] // '/myapp/config/';
my $state_dir = $ENV{RESUMABLE_WATCH_STATE_DIR} || '/tmp';
my $state_path = "$state_dir/resumable_watch_" . _safe_name($prefix) . ".rev";
my $client = EV::Etcd->new(endpoints => ['127.0.0.1:2379'], max_retries => 5);
my $last_rev = read_last_rev();
my ($watch, $retry_timer);
if ($last_rev) {
say "[resume] last seen revision: $last_rev, replaying what came after";
start_watch($last_rev + 1);
} else {
say "[resume] no saved revision, listing the current state";
relist();
}
# Persist progress periodically and on shutdown
my $persist_timer = EV::timer(5, 5, \&save_last_rev);
my $shutdown = sub {
save_last_rev();
say "[resume] saved revision $last_rev to $state_path on shutdown";
EV::break;
};
my $sigint = EV::signal('INT', $shutdown);
my $sigterm = EV::signal('TERM', $shutdown);
EV::run;
# --------------------------------------------------------------------------
sub start_watch {
my $start_rev = shift;
say "[watch] starting from revision $start_rev";
$watch = $client->watch($prefix, {
prefix => 1,
start_revision => $start_rev,
progress_notify => 1,
}, sub {
my ($r, $err) = @_;
if ($err) {
if ($err->{compact_revision}) {
warn "[watch] history up to revision $err->{compact_revision}"
. " is compacted, listing the current state\n";
return relist();
}
# Reconnects are exhausted or the server ended the watch
warn "[watch] error: $err->{message}, retrying in 2s\n";
$retry_timer = EV::timer(2, 0, sub { start_watch($last_rev + 1) });
return;
}
for my $ev (@{$r->{events}}) {
my $kv = $ev->{kv};
say "[$ev->{type}] $kv->{key} = $kv->{value}";
$last_rev = $kv->{mod_revision} if $kv->{mod_revision} > $last_rev;
}
# A progress notification (no events) covers everything up to its
# revision; the created response does not, since history may still
# be replaying
$last_rev = $r->{header}{revision}
if !$r->{created} && !@{$r->{events}} && $r->{header}{revision} > $last_rev;
});
}
# Snapshot of the prefix, then watch everything after it
sub relist {
( run in 1.427 second using v1.01-cache-2.11-cpan-16aabcbfbcb )