DBD-Patroni
view release on metacpan or search on metacpan
t/03-failover.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use DBI;
# Skip if not in integration test environment
unless ( $ENV{PATRONI_URLS} && $ENV{TEST_FAILOVER} ) {
plan skip_all =>
'PATRONI_URLS and TEST_FAILOVER not set, skipping failover tests';
}
use_ok('DBD::Patroni');
use LWP::UserAgent;
use JSON;
my $patroni_urls = $ENV{PATRONI_URLS};
my $user = $ENV{PGUSER} || 'testuser';
my $pass = $ENV{PGPASSWORD} || 'testpass';
my $dbname = $ENV{PGDATABASE} || 'testdb';
my $sslmode = $ENV{PGSSLMODE} || 'disable';
my $dsn = "dbi:Patroni:dbname=$dbname;sslmode=$sslmode;patroni_url=$patroni_urls";
# Helper to get cluster info
sub get_cluster_info {
my $ua = LWP::UserAgent->new( timeout => 5 );
for my $url ( split /,/, $patroni_urls ) {
my $resp = $ua->get($url);
next unless $resp->is_success;
my $data = eval { decode_json( $resp->decoded_content ) };
next unless $data && $data->{members};
my ($leader) = grep { $_->{role} eq 'leader' } @{ $data->{members} };
my @replicas = grep { $_->{role} ne 'leader' } @{ $data->{members} };
return {
leader => $leader,
replicas => \@replicas,
members => $data->{members},
};
}
return undef;
}
# Helper to trigger failover via Patroni API
sub trigger_failover {
my ($new_leader) = @_;
my $ua = LWP::UserAgent->new( timeout => 10 );
# Find current leader's API endpoint
my $info = get_cluster_info();
return 0 unless $info && $info->{leader};
my $leader_host = $info->{leader}{host};
my $failover_url = "http://${leader_host}:8008/failover";
my $resp = $ua->post(
$failover_url,
'Content-Type' => 'application/json',
Content => encode_json( { candidate => $new_leader } ),
);
diag( "Failover response: " . $resp->status_line );
diag( "Failover body: " . $resp->decoded_content ) if !$resp->is_success;
return $resp->is_success;
}
# Wait for all replicas to be running
sub wait_for_replicas {
my $max_attempts = shift || 60;
for my $i ( 1 .. $max_attempts ) {
my $info = get_cluster_info();
next unless $info;
# Count nodes that are running or streaming (replicas in sync)
my @ready =
grep { $_->{state} eq 'running' || $_->{state} eq 'streaming' }
@{ $info->{members} };
if ( @ready >= 3 ) {
diag("All 3 nodes are ready");
return 1;
}
# Show all states for debugging
my $states =
join( ", ", map { "$_->{name}:$_->{state}" } @{ $info->{members} } );
diag("Attempt $i/$max_attempts: $states");
sleep 5;
}
return 0;
}
# Wait for all nodes to be ready before starting tests
diag("Waiting for all cluster nodes to be ready...");
wait_for_replicas(60); # 60 attempts x 5 seconds = 5 minutes max
# Test 1: Detect current leader
subtest 'Detect current leader' => sub {
my $info = get_cluster_info();
ok( $info, 'Got cluster info' );
ok( $info->{leader}, 'Found leader' );
ok( @{ $info->{replicas} } >= 1, 'Found at least one replica' );
diag( "Current leader: " . $info->{leader}{host} );
diag( "Replicas: "
. join( ", ", map { $_->{host} } @{ $info->{replicas} } ) );
( run in 1.348 second using v1.01-cache-2.11-cpan-ceb78f64989 )