Apache-Session-Browseable
view release on metacpan or search on metacpan
t/Apache-Session-Browseable-Store-Patroni.t view on Meta::CPAN
# Simulate caching for first datasource
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds1} = {
leader => {
host => '10.0.0.1',
port => 5432,
time => time()
}
};
# Simulate caching for second datasource
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds2} = {
leader => {
host => '10.0.0.2',
port => 5433,
time => time()
}
};
# Verify they are independent
is(
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds1}
->{leader}->{host},
'10.0.0.1', 'Multi-source: first datasource has correct host'
);
is(
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds2}
->{leader}->{host},
'10.0.0.2', 'Multi-source: second datasource has correct host'
);
# Verify ports are independent
is(
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds1}
->{leader}->{port},
5432, 'Multi-source: first datasource has correct port'
);
is(
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds2}
->{leader}->{port},
5433, 'Multi-source: second datasource has correct port'
);
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test _useCachedLeader
{
no warnings 'once';
my $ds = 'dbi:Pg:dbname=testdb';
my $store = $package->new;
$store->{_originalDataSource} = $ds;
# No cache - should return 0
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $args = { DataSource => $ds, PatroniCacheTTL => 60 };
# Capture STDERR
my $stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->_useCachedLeader( $args, $ds, "Test reason" );
is( $result, 0, '_useCachedLeader: returns 0 when no cache' );
}
# With valid cache
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds} = {
leader => {
host => '10.0.0.5',
port => 5432,
time => time() - 10 # 10 seconds ago
}
};
$stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->_useCachedLeader( $args, $ds, "Test reason" );
is( $result, 1, '_useCachedLeader: returns 1 when cache valid' );
}
like( $args->{DataSource}, qr/host=10\.0\.0\.5/,
'_useCachedLeader: updates DataSource with cached host' );
like( $args->{DataSource}, qr/port=5432/,
'_useCachedLeader: updates DataSource with cached port' );
# With expired cache
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds} = {
leader => {
host => '10.0.0.6',
port => 5433,
time => time() - 120 # 2 minutes ago, expired
}
};
$args = { DataSource => $ds, PatroniCacheTTL => 60 };
$stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->_useCachedLeader( $args, $ds, "Test reason" );
is( $result, 0, '_useCachedLeader: returns 0 when cache expired' );
}
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test circuit breaker logic in checkMaster (without actual HTTP calls)
{
no warnings 'once';
my $ds = 'dbi:Pg:dbname=circuitdb';
my $store = $package->new;
$store->{_originalDataSource} = $ds;
# Set up circuit breaker as triggered
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds} = {
lastFailure => time() - 10, # Failed 10 seconds ago
leader => {
host => '10.0.0.7',
port => 5432,
time => time() - 5 # Cached 5 seconds ago
}
};
my $args = {
DataSource => $ds,
PatroniUrl => 'http://fake.patroni:8008/cluster',
PatroniCircuitBreakerDelay => 30,
PatroniCacheTTL => 60,
};
# Circuit breaker should be active (failed 10s ago, delay is 30s)
# It should use cached leader instead of calling API
my $stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->checkMaster($args);
is( $result, 1, 'Circuit breaker: uses cached leader when active' );
}
like(
$stderr,
qr/Circuit breaker active/,
'Circuit breaker: prints appropriate message'
);
like( $args->{DataSource}, qr/host=10\.0\.0\.7/,
'Circuit breaker: uses cached host' );
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test that circuit breaker expires
{
no warnings 'once';
my $ds = 'dbi:Pg:dbname=expiredcb';
my $store = $package->new;
$store->{_originalDataSource} = $ds;
# Circuit breaker triggered long ago (should be expired)
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds} = {
lastFailure => time() - 60, # Failed 60 seconds ago
leader => {
host => '10.0.0.8',
port => 5432,
time => time() - 50 # Still valid cache
}
};
my $args = {
DataSource => $ds,
PatroniUrl => 'http://nonexistent.host:8008/cluster',
PatroniCircuitBreakerDelay => 30, # Expired (60 > 30)
PatroniCacheTTL => 120,
};
# Circuit breaker should NOT be active (failed 60s ago, delay is 30s)
# It will try to call API (which will fail) then use cache
my $stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
# This will try HTTP (fail) then fallback to cache
my $result = $store->checkMaster($args);
is( $result, 1,
'Circuit breaker expired: uses cache after API failure' );
}
like(
$stderr,
qr/Patroni API unavailable/,
'Circuit breaker expired: API was tried'
);
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test default values
{
my $store = $package->new;
my $ds = 'dbi:Pg:dbname=defaults';
$store->{_originalDataSource} = $ds;
# Test with minimal args
my $args = {
DataSource => $ds,
PatroniUrl => 'http://fake:8008/cluster',
};
# Without cache, checkMaster will try HTTP and fail
# This tests that defaults are applied correctly
my $stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
# Store the time before calling checkMaster
my $before = time();
my $result = $store->checkMaster($args);
# Verify failure was recorded with correct circuit breaker
my $cache =
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds};
ok( $cache->{lastFailure} >= $before,
'Default circuit breaker delay: failure recorded' );
}
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test custom TTL values
{
no warnings 'once';
my $ds = 'dbi:Pg:dbname=customttl';
# Set up cache with known age
$Apache::Session::Browseable::Store::Patroni::patroniCache{$ds} = {
leader => {
host => '10.0.0.9',
port => 5432,
time => time() - 45 # 45 seconds ago
}
};
my $store = $package->new;
$store->{_originalDataSource} = $ds;
# With 30s TTL, cache should be expired
my $args1 = { DataSource => $ds, PatroniCacheTTL => 30 };
my $stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->_useCachedLeader( $args1, $ds, "Test" );
is( $result, 0, 'Custom TTL 30s: cache expired at 45s' );
}
# With 60s TTL, cache should be valid
my $args2 = { DataSource => $ds, PatroniCacheTTL => 60 };
$stderr = '';
{
local *STDERR;
open STDERR, '>', \$stderr;
my $result = $store->_useCachedLeader( $args2, $ds, "Test" );
is( $result, 1, 'Custom TTL 60s: cache valid at 45s' );
}
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
# Test module can be loaded
use_ok('Apache::Session::Browseable::Patroni');
# Test JSON parsing and leader validation with mocked HTTP using LWP::Protocol::PSGI
SKIP: {
skip 'LWP::Protocol::PSGI and JSON not available', 10
unless eval { require LWP::Protocol::PSGI; require JSON; 1 };
my $package = 'Apache::Session::Browseable::Store::Patroni';
my $ds = 'dbi:Pg:dbname=mocktest';
# Helper to create PSGI app with given response
my $make_app = sub {
my ($json_data) = @_;
return sub {
my $env = shift;
return [
200,
[ 'Content-Type' => 'application/json' ],
[ JSON::to_json($json_data) ]
];
};
};
# Test 1: Valid leader response
{
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $guard = LWP::Protocol::PSGI->register(
$make_app->(
{
members => [
{
role => 'leader',
host => '10.0.0.100',
port => 5432,
state => 'running'
},
{
role => 'replica',
host => '10.0.0.101',
port => 5432,
state => 'streaming'
}
]
}
)
);
my $store = $package->new;
$store->{_originalDataSource} = $ds;
my $args = {
DataSource => $ds,
PatroniUrl => 'http://mock:8008/cluster'
};
my $stderr = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$stderr;
$result = $store->checkMaster($args);
}
is( $result, 1, 'Valid leader: checkMaster returns 1' );
like( $args->{DataSource}, qr/host=10\.0\.0\.100/,
'Valid leader: correct host in DataSource' );
}
# Test 2: Split-brain detection (multiple leaders)
{
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $guard = LWP::Protocol::PSGI->register(
$make_app->(
{
members => [
{
role => 'leader',
host => '10.0.0.100',
port => 5432,
state => 'running'
},
{
role => 'leader',
host => '10.0.0.101',
port => 5432,
state => 'running'
}
]
}
)
);
my $store = $package->new;
$store->{_originalDataSource} = $ds;
my $args = {
DataSource => $ds,
PatroniUrl => 'http://mock:8008/cluster'
};
my $stderr = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$stderr;
$result = $store->checkMaster($args);
}
is( $result, 0, 'Split-brain: checkMaster returns 0' );
like(
$stderr,
qr/Multiple leaders detected/,
'Split-brain: warning message'
);
}
# Test 3: Leader not in running state
{
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $guard = LWP::Protocol::PSGI->register(
$make_app->(
{
members => [
{
role => 'leader',
host => '10.0.0.100',
port => 5432,
state => 'starting'
}
]
}
)
);
my $store = $package->new;
$store->{_originalDataSource} = $ds;
my $args = {
DataSource => $ds,
PatroniUrl => 'http://mock:8008/cluster'
};
my $stderr = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$stderr;
$result = $store->checkMaster($args);
}
is( $result, 0, 'Leader starting: checkMaster returns 0' );
like(
$stderr,
qr/not in running state/,
'Leader starting: warning message'
);
}
# Test 4: Leader missing host
{
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $guard = LWP::Protocol::PSGI->register(
$make_app->(
{
members => [
{
role => 'leader',
port => 5432,
state => 'running'
}
]
}
)
);
my $store = $package->new;
$store->{_originalDataSource} = $ds;
my $args = {
DataSource => $ds,
PatroniUrl => 'http://mock:8008/cluster'
};
my $stderr = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$stderr;
$result = $store->checkMaster($args);
}
is( $result, 0, 'Leader missing host: checkMaster returns 0' );
like(
$stderr,
qr/missing host or port/,
'Leader missing host: warning message'
);
}
# Test 5: No leader found
{
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
my $guard = LWP::Protocol::PSGI->register(
$make_app->(
{
members => [
{
role => 'replica',
host => '10.0.0.101',
port => 5432,
state => 'streaming'
}
]
}
)
);
my $store = $package->new;
$store->{_originalDataSource} = $ds;
my $args = {
DataSource => $ds,
PatroniUrl => 'http://mock:8008/cluster'
};
my $stderr = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$stderr;
$result = $store->checkMaster($args);
}
is( $result, 0, 'No leader: checkMaster returns 0' );
like( $stderr, qr/No leader found/, 'No leader: warning message' );
}
# Clear cache
%Apache::Session::Browseable::Store::Patroni::patroniCache = ();
}
( run in 0.726 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )