DBD-Patroni

 view release on metacpan or  search on metacpan

lib/DBD/Patroni.pm  view on Meta::CPAN

#
#  You may distribute under the terms of either the GNU General Public
#  License or the Artistic License, as specified in the Perl README file.

use strict;
use warnings;

package DBD::Patroni;

use DBI;
require DBD::Pg;

our $VERSION = '0.03';
our $drh     = undef;    # Driver handle
our $err     = 0;        # DBI error code
our $errstr  = '';       # DBI error string
our $state   = '';       # DBI state
our $rr_idx  = 0;        # Round-robin index for replica selection

# Load submodules
require DBD::Patroni::dr;
require DBD::Patroni::db;
require DBD::Patroni::st;

# DBI driver method - required for DBI->connect("dbi:Patroni:...")
sub driver {
    return $drh if $drh;
    my ( $class, $attr ) = @_;

    $class .= "::dr";

    $drh = DBI::_new_drh(
        $class,
        {
            Name        => 'Patroni',
            Version     => $VERSION,
            Attribution => 'DBD::Patroni by Xavier Guimard',
        }
    );
    return $drh;
}

# Discover Patroni cluster via REST API
sub _discover_cluster {
    my ( $urls, $timeout ) = @_;
    $timeout //= 3;

    require LWP::UserAgent;
    require JSON;

    my $ua = LWP::UserAgent->new(
        timeout   => $timeout,
        env_proxy => 1,
    );

    for my $url ( split /[,\s]+/, $urls ) {
        next unless $url;
        my $resp = $ua->get($url);
        next unless $resp->is_success;

        my $data = eval { JSON::decode_json( $resp->decoded_content ) };
        next if $@ or !$data->{members} or ref( $data->{members} ) ne 'ARRAY';

        my ($leader) = grep { $_->{role} eq 'leader' } @{ $data->{members} };
        my @replicas = grep { $_->{role} ne 'leader' } @{ $data->{members} };

        return ( $leader, @replicas ) if $leader;
    }
    return;
}

# Select a replica based on load balancing mode
sub _select_replica {
    my ( $replicas, $mode ) = @_;
    return undef unless $replicas && @$replicas;

    $mode //= 'round_robin';

    if ( $mode eq 'random' ) {
        return $replicas->[ int( rand(@$replicas) ) ];
    }
    elsif ( $mode eq 'leader_only' ) {
        return undef;
    }
    else {    # round_robin
        return $replicas->[ $rr_idx++ % @$replicas ];
    }
}

# Parse and extract Patroni parameters from DSN
sub _parse_dsn {
    my ($dsn) = @_;
    my %params;
    my @remaining;

    for my $part ( split /;/, $dsn ) {
        if ( $part =~ /^(patroni_url|patroni_lb|patroni_timeout)=(.*)$/i ) {
            $params{ lc($1) } = $2;
        }
        else {
            push @remaining, $part;
        }
    }

    return ( join( ';', @remaining ), \%params );
}

# Build DSN with host/port, cleaning up any existing host/port params
sub _build_dsn {
    my ( $dsn, $host, $port ) = @_;

    # Remove existing host/port parameters
    $dsn =~ s/(?:host|port)=[^;]*;?//gi;

    # Clean up multiple semicolons and leading/trailing semicolons
    $dsn =~ s/;+/;/g;
    $dsn =~ s/^;|;$//g;

    # Append new host/port
    return "$dsn;host=$host;port=$port";
}



( run in 2.006 seconds using v1.01-cache-2.11-cpan-600a1bdf6e4 )