feat(perl-client): 新增 Perl 客户端 SDK 及 demo,各客户端版本统一为 1.0.3
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package Kenaito::Conf;
|
||||
use strict;
|
||||
use warnings;
|
||||
use Kenaito::Conf::Client;
|
||||
use Kenaito::Conf::Config;
|
||||
|
||||
our $VERSION = '1.0.3';
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,33 @@
|
||||
package Kenaito::Conf::Cache;
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::PP;
|
||||
use File::Spec;
|
||||
|
||||
use constant CACHE_FILE => 'config-cache.json';
|
||||
|
||||
sub new {
|
||||
my ($class, $cache_dir) = @_;
|
||||
$cache_dir //= '.';
|
||||
return bless { dir => $cache_dir }, $class;
|
||||
}
|
||||
|
||||
sub write {
|
||||
my ($self, $data) = @_;
|
||||
my $path = File::Spec->catfile($self->{dir}, CACHE_FILE);
|
||||
open my $fh, '>:encoding(UTF-8)', $path or return;
|
||||
print $fh JSON::PP->new->utf8->encode($data);
|
||||
close $fh;
|
||||
}
|
||||
|
||||
sub read {
|
||||
my ($self) = @_;
|
||||
my $path = File::Spec->catfile($self->{dir}, CACHE_FILE);
|
||||
open my $fh, '<:encoding(UTF-8)', $path or return undef;
|
||||
local $/;
|
||||
my $json = <$fh>;
|
||||
close $fh;
|
||||
return eval { JSON::PP->new->utf8->decode($json) };
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,179 @@
|
||||
package Kenaito::Conf::Client;
|
||||
use strict;
|
||||
use warnings;
|
||||
use HTTP::Tiny;
|
||||
use JSON::PP;
|
||||
use Kenaito::Conf::Config;
|
||||
use Kenaito::Conf::Cache;
|
||||
use Kenaito::Conf::Listener;
|
||||
|
||||
sub new {
|
||||
my ($class, %args) = @_;
|
||||
my $config = $args{config};
|
||||
die "config is required" unless $config;
|
||||
|
||||
my $self = bless {
|
||||
config => $config,
|
||||
cache => Kenaito::Conf::Cache->new($config->{cache_dir}),
|
||||
listener => Kenaito::Conf::Listener->new(),
|
||||
configs => {},
|
||||
last_time => 0,
|
||||
started => 0,
|
||||
stop => 0,
|
||||
http => HTTP::Tiny->new(timeout => 10),
|
||||
}, $class;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub on_change {
|
||||
my ($self, $handler) = @_;
|
||||
$self->{listener}->on_change($handler);
|
||||
}
|
||||
|
||||
sub get {
|
||||
my ($self, $key) = @_;
|
||||
return $self->{configs}{$key};
|
||||
}
|
||||
|
||||
sub get_all {
|
||||
my ($self) = @_;
|
||||
return { %{$self->{configs}} };
|
||||
}
|
||||
|
||||
sub start {
|
||||
my ($self) = @_;
|
||||
return if $self->{started};
|
||||
$self->{started} = 1;
|
||||
|
||||
eval {
|
||||
$self->_fetch_all();
|
||||
1;
|
||||
} or do {
|
||||
my $err = $@;
|
||||
warn "[ConfigClient] fetch all configs error: $err, trying cache\n";
|
||||
my $cached = $self->{cache}->read();
|
||||
if ($cached) {
|
||||
$self->_apply_configs($cached);
|
||||
my $count = scalar keys %{$self->{configs}};
|
||||
print "[ConfigClient] loaded $count configs from cache\n";
|
||||
} else {
|
||||
die "fetch configs failed and no cache: $err";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
sub stop {
|
||||
my ($self) = @_;
|
||||
$self->{stop} = 1;
|
||||
}
|
||||
|
||||
sub sync_once {
|
||||
my ($self) = @_;
|
||||
eval {
|
||||
$self->_fetch_changed();
|
||||
1;
|
||||
} or do {
|
||||
warn "[ConfigClient] sync error: $@\n";
|
||||
};
|
||||
}
|
||||
|
||||
sub _fetch_all {
|
||||
my ($self) = @_;
|
||||
my $url = "$self->{config}{base_url}/api/fetchAllConfigs";
|
||||
my $payload = JSON::PP->new->utf8->encode({
|
||||
appName => $self->{config}{app_name},
|
||||
env => $self->{config}{env},
|
||||
version => $self->{config}{version},
|
||||
ignoreGlobal => $self->{config}{ignore_global} ? \1 : \0,
|
||||
token => $self->{config}{token},
|
||||
});
|
||||
|
||||
my $resp = $self->{http}->post($url, {
|
||||
content => $payload,
|
||||
headers => { 'Content-Type' => 'application/json' },
|
||||
});
|
||||
|
||||
die "HTTP error: $resp->{status} $resp->{reason}" unless $resp->{success};
|
||||
|
||||
my $data = JSON::PP->new->utf8->decode($resp->{content});
|
||||
|
||||
if (exists $data->{code} && $data->{code} != 0) {
|
||||
die "server error: " . ($data->{msg} // 'unknown');
|
||||
}
|
||||
|
||||
if (my $ts = $data->{'latest.fetch.timestamp'}) {
|
||||
$self->{last_time} = int($ts);
|
||||
}
|
||||
|
||||
$self->{cache}->write($data);
|
||||
$self->_apply_configs($data);
|
||||
|
||||
my $count = scalar keys %{$self->{configs}};
|
||||
print "[ConfigClient] fetched $count configs\n";
|
||||
}
|
||||
|
||||
sub _fetch_changed {
|
||||
my ($self) = @_;
|
||||
my $url = "$self->{config}{base_url}/api/fetchChangedConfigs";
|
||||
my $payload = JSON::PP->new->utf8->encode({
|
||||
appName => $self->{config}{app_name},
|
||||
env => $self->{config}{env},
|
||||
version => $self->{config}{version},
|
||||
lastTime => $self->{last_time},
|
||||
ignoreGlobal => $self->{config}{ignore_global} ? \1 : \0,
|
||||
token => $self->{config}{token},
|
||||
});
|
||||
|
||||
my $resp = $self->{http}->post($url, {
|
||||
content => $payload,
|
||||
headers => { 'Content-Type' => 'application/json' },
|
||||
});
|
||||
|
||||
die "HTTP error: $resp->{status} $resp->{reason}" unless $resp->{success};
|
||||
|
||||
my $result = JSON::PP->new->utf8->decode($resp->{content});
|
||||
|
||||
if (($result->{code} // -1) != 0) {
|
||||
die "server error code: $result->{code}";
|
||||
}
|
||||
|
||||
my $changed_data = $result->{data} || {};
|
||||
|
||||
if (my $ts = $changed_data->{'latest.fetch.timestamp'}) {
|
||||
$self->{last_time} = int($ts);
|
||||
}
|
||||
delete $changed_data->{'latest.fetch.timestamp'};
|
||||
|
||||
# handle deleted keys
|
||||
if (my $deleted_keys_str = delete $changed_data->{deletedKeys}) {
|
||||
my @deleted = split /,/, $deleted_keys_str;
|
||||
for my $key (@deleted) {
|
||||
delete $self->{configs}{$key};
|
||||
}
|
||||
my $count = scalar @deleted;
|
||||
print "[ConfigClient] $count configs deleted: " . join(', ', @deleted) . "\n";
|
||||
}
|
||||
|
||||
if (%$changed_data) {
|
||||
my %changed;
|
||||
for my $k (keys %$changed_data) {
|
||||
$self->{configs}{$k} = $changed_data->{$k};
|
||||
$changed{$k} = $changed_data->{$k};
|
||||
}
|
||||
$self->{listener}->notify(\%changed);
|
||||
my $count = scalar keys %changed;
|
||||
print "[ConfigClient] $count configs changed: " . join(', ', keys %changed) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub _apply_configs {
|
||||
my ($self, $data) = @_;
|
||||
for my $k (keys %$data) {
|
||||
next if $k =~ /^kenaito\./ || $k eq 'latest.fetch.timestamp';
|
||||
next unless defined $data->{$k} && !ref $data->{$k};
|
||||
$self->{configs}{$k} = $data->{$k};
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,19 @@
|
||||
package Kenaito::Conf::Config;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub new {
|
||||
my ($class, %args) = @_;
|
||||
return bless {
|
||||
base_url => $args{base_url} // 'http://localhost:8080',
|
||||
app_name => $args{app_name} // '',
|
||||
env => $args{env} // 'dev',
|
||||
version => $args{version} // 'latest',
|
||||
token => $args{token} // '',
|
||||
sync_interval => $args{sync_interval} // 60,
|
||||
ignore_global => $args{ignore_global} // 0,
|
||||
cache_dir => $args{cache_dir} // '.',
|
||||
}, $class;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,22 @@
|
||||
package Kenaito::Conf::Listener;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub new {
|
||||
my ($class) = @_;
|
||||
return bless { handlers => [] }, $class;
|
||||
}
|
||||
|
||||
sub on_change {
|
||||
my ($self, $handler) = @_;
|
||||
push @{$self->{handlers}}, $handler;
|
||||
}
|
||||
|
||||
sub notify {
|
||||
my ($self, $changed) = @_;
|
||||
for my $h (@{$self->{handlers}}) {
|
||||
eval { $h->($changed); 1 } or warn "[ConfigClient] change handler error: $@\n";
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
Reference in New Issue
Block a user