App-Config-Chronicle
view release on metacpan or search on metacpan
lib/App/Config/Chronicle.pm view on Meta::CPAN
default: 10
global: 1
admins:
description: "Are we on Production?"
isa: ArrayRef
default: []
Every attribute is very intuitive. If an item is global, you can change its value and the value will be stored into chronicle database by calling the method C<save_dynamic>.
=head1 SUBROUTINES/METHODS (LEGACY)
=cut
use Moose;
use namespace::autoclean;
use YAML::XS qw(LoadFile);
use App::Config::Chronicle::Attribute::Section;
use App::Config::Chronicle::Attribute::Global;
use Data::Hash::DotNotation;
use Data::Chronicle::Reader;
use Data::Chronicle::Writer;
use Data::Chronicle::Subscriber;
=head2 REDIS_HISTORY_TTL
The maximum length of time (in seconds) that a cached history entry will stay in Redis.
=cut
use constant REDIS_HISTORY_TTL => 7 * 86400; # 7 days
=head2 definition_yml
The YAML file that store the configuration
=cut
has definition_yml => (
is => 'ro',
isa => 'Str',
required => 1,
);
=head2 chronicle_reader
The chronicle store that configurations can be fetch from it. It should be an instance of L<Data::Chronicle::Reader>.
But user is free to implement any storage backend he wants if it is implemented with a 'get' method.
=cut
has chronicle_reader => (
is => 'ro',
isa => 'Data::Chronicle::Reader',
required => 1,
);
=head2 chronicle_writer
The chronicle store that updated configurations can be stored into it. It should be an instance of L<Data::Chronicle::Writer>.
But user is free to implement any storage backend he wants if it is implemented with a 'set' method.
=cut
has chronicle_writer => (
is => 'rw',
isa => 'Data::Chronicle::Writer',
);
=head2 chronicle_subscriber
The chronicle connection that can notify via callbacks when particular configuration items have a new value set. It should be an instance of L<Data::Chronicle::Subscriber>.
=cut
has chronicle_subscriber => (
is => 'ro',
isa => 'Data::Chronicle::Subscriber'
);
has setting_namespace => (
is => 'ro',
isa => 'Str',
default => 'app_settings',
);
has setting_name => (
is => 'ro',
isa => 'Str',
required => 1,
default => 'settings1',
);
=head2 refresh_interval
How much time (in seconds) should pass between L<check_for_update> invocations until
it actually will do (a bit heavy) lookup for settings in redis.
Default value is 10 seconds
=cut
has refresh_interval => (
is => 'ro',
isa => 'Num',
required => 1,
default => 10,
);
has _updated_at => (
is => 'rw',
isa => 'Num',
required => 1,
default => 0,
);
# definitions database
has _defdb => (
is => 'rw',
lazy => 1,
default => sub { LoadFile(shift->definition_yml) },
);
has 'data_set' => (
is => 'ro',
lazy_build => 1,
);
sub _build_class {
my $self = shift;
$self->_create_attributes($self->_defdb, $self);
return;
}
sub _create_attributes {
my $self = shift;
my $definitions = shift;
my $containing_section = shift;
$containing_section->meta->make_mutable;
foreach my $definition_key (keys %{$definitions}) {
$self->_validate_key($definition_key, $containing_section);
my $definition = $definitions->{$definition_key};
if ($definition->{isa} eq 'section') {
$self->_create_section($containing_section, $definition_key, $definition);
$self->_create_attributes($definition->{contains}, $containing_section->$definition_key);
} elsif ($definition->{global}) {
$self->_create_global_attribute($containing_section, $definition_key, $definition);
} else {
$self->_create_generic_attribute($containing_section, $definition_key, $definition);
}
}
$containing_section->meta->make_immutable;
return;
}
sub _create_section {
my $self = shift;
my $section = shift;
my $name = shift;
my $definition = shift;
my $writer = "_$name";
my $path_config = {};
if ($section->isa('App::Config::Chronicle::Attribute::Section')) {
$path_config = {parent_path => $section->path};
}
my $new_section = Moose::Meta::Class->create_anon_class(superclasses => ['App::Config::Chronicle::Attribute::Section'])->new_object(
lib/App/Config/Chronicle.pm view on Meta::CPAN
sub _create_generic_attribute {
my $self = shift;
my $section = shift;
my $name = shift;
my $definition = shift;
$self->_add_attribute('App::Config::Chronicle::Attribute', $section, $name, $definition);
return;
}
sub _add_attribute {
my $self = shift;
my $attr_class = shift;
my $section = shift;
my $name = shift;
my $definition = shift;
my $fake_name = "a_$name";
my $writer = "_$fake_name";
my $attribute = $attr_class->new(
name => $name,
definition => $definition,
parent_path => $section->path,
data_set => $self->data_set,
)->build;
$section->meta->add_attribute(
$fake_name,
is => 'ro',
handles => {
$name => 'value',
'has_' . $name => 'has_value',
},
documentation => $definition->{description},
writer => $writer,
);
$section->$writer($attribute);
return $attribute;
}
sub _validate_key {
my $self = shift;
my $key = shift;
my $section = shift;
if (grep { $key eq $_ } qw(path parent_path name definition version data_set check_for_update save_dynamic refresh_interval)) {
die "Variable with name $key found under "
. $section->path
. ".\n$key is an internally used variable and cannot be reused, please use a different name";
}
return;
}
=head2 check_for_update
check and load updated settings from chronicle db
Checks at most every C<refresh_interval> unless forced with
a truthy first argument
=cut
sub check_for_update {
my ($self, $force) = @_;
return unless $force or $self->_has_refresh_interval_passed();
$self->_updated_at(Time::HiRes::time());
# do check in Redis
my $data_set = $self->data_set;
my $app_settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name);
my $db_version;
if ($app_settings and $data_set) {
$db_version = $app_settings->{_rev};
unless ($data_set->{version} and $db_version and $db_version eq $data_set->{version}) {
# refresh all
$self->_add_app_setttings($data_set, $app_settings);
}
}
return $db_version;
}
=head2 save_dynamic
Save dynamic settings into chronicle db
=cut
sub save_dynamic {
my $self = shift;
my ($package, $filename, $line) = caller;
warnings::warnif deprecated => "Deprecated call used (save_dynamic). Called from package: $package | file: $filename | line: $line";
return $self->_save_dynamic();
}
sub _save_dynamic {
my $self = shift;
my $settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name) || {};
#Cleanup globals
my $global = Data::Hash::DotNotation->new();
foreach my $key (keys %{$self->dynamic_settings_info->{global}}) {
if ($self->data_set->{global}->key_exists($key)) {
$global->set($key, $self->data_set->{global}->get($key));
}
}
$settings->{global} = $global->data;
$settings->{_rev} = Time::HiRes::time();
$self->chronicle_writer->set($self->setting_namespace, $self->setting_name, $settings, Date::Utility->new);
# since we now have the most recent data, we better set the
# local version as well.
$self->data_set->{version} = $settings->{_rev};
$self->_updated_at($settings->{_rev});
return 1;
}
=head2 current_revision
Loads setting from chronicle reader and returns the last revision
It is more likely that you want L</loaded_revision> in regular use
=cut
sub current_revision {
my $self = shift;
my $settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name);
return $settings->{_rev};
}
=head2 loaded_revision
Returns the revision loaded and served by this instance
This may not reflect the latest stored version in the Chronicle persistence.
However, it is the revision of the data which will be returned when
querying this instance
=cut
sub loaded_revision {
my $self = shift;
return $self->data_set->{version};
}
sub _build_data_set {
my $self = shift;
# relatively small yaml, so loading it shouldn't be expensive.
my $data_set->{app_config} = Data::Hash::DotNotation->new(data => {});
$self->_add_app_setttings($data_set, $self->chronicle_reader->get($self->setting_namespace, $self->setting_name) || {});
return $data_set;
}
sub _add_app_setttings {
my $self = shift;
my $data_set = shift;
my $app_settings = shift;
if ($app_settings) {
$data_set->{global} = Data::Hash::DotNotation->new(data => $app_settings->{global});
$data_set->{version} = $app_settings->{_rev};
}
return;
}
has dynamic_settings_info => (
is => 'ro',
isa => 'HashRef',
default => sub { {} },
);
sub _add_dynamic_setting_info {
my $self = shift;
my $path = shift;
my $definition = shift;
$self->dynamic_settings_info = {} unless ($self->dynamic_settings_info);
$self->dynamic_settings_info->{global} = {} unless ($self->dynamic_settings_info->{global});
$self->dynamic_settings_info->{global}->{$path} = {
type => $definition->{isa},
default => $definition->{default},
description => $definition->{description}};
return;
}
=head1 SUBROUTINES/METHODS
######################################################
###### Start new API
######################################################
=head2 local_caching
If local_caching is set to the true then key-value pairs stored in Redis will be cached locally.
Calling update_cache will update the local cache with any changes from Redis.
refresh_interval defines (in seconds) the minimum time between seqequent updates.
Calls to get on this object will only ever access the cache.
Calls to set on this object will immediately update the values in the local cache and Redis.
=cut
has local_caching => (
isa => 'Bool',
is => 'ro',
default => 0,
);
=head2 update_cache
Loads latest values from data chronicle into local cache.
Calls to this method are rate-limited by C<refresh_interval>.
=cut
sub update_cache {
my $self = shift;
die 'Local caching not enabled' unless $self->local_caching;
return unless $self->_has_refresh_interval_passed();
$self->_updated_at(Time::HiRes::time());
return unless $self->_is_cache_stale();
my $keys = [$self->dynamic_keys(), '_global_rev'];
my @all_entries = $self->_retrieve_objects_from_chron($keys);
$self->_store_objects_in_cache({map { $keys->[$_] => $all_entries[$_] } (0 .. $#$keys)});
return 1;
}
sub _has_refresh_interval_passed {
my $self = shift;
my $now = Time::HiRes::time();
my $prev_update = $self->_updated_at;
my $time_since_prev_update = $now - $prev_update;
return ($time_since_prev_update >= $self->refresh_interval);
}
sub _is_cache_stale {
my $self = shift;
my @rev_cache = $self->_retrieve_objects_from_cache(['_global_rev']);
my @rev_chron = $self->_retrieve_objects_from_chron(['_global_rev']);
return !($rev_cache[0] && $rev_chron[0] && $rev_cache[0]->{data} eq $rev_chron[0]->{data});
}
=head2 global_revision
Returns the global revision version of the config chronicle.
This will correspond to the last time any of values were changed.
=cut
sub global_revision {
my $self = shift;
return $self->get('_global_rev');
}
=head2 set
Takes a hashref of key->value pairs and atomically sets them in config chronicle
Example:
set({key1 => 'value1', key2 => 'value2', key3 => 'value3',...});
=cut
sub set {
my ($self, $pairs) = @_;
die 'cannot set when $self->chronicle_writer is undefined' unless $self->chronicle_writer;
my $rev_obj = Date::Utility->new;
my $rev_epoch = $rev_obj->{epoch};
$self->_key_is_dynamic($_) or die "Cannot set with key: $_ | Key must be defined with 'global: 1'" foreach keys %$pairs;
$pairs->{_global_rev} = $rev_epoch;
my %key_objs_hash = pairmap {
$a => {
data => $b,
_local_rev => $rev_epoch
} } %$pairs;
$self->_store_objects(\%key_objs_hash, $rev_obj);
######
# Temporary adapter code
######
$self->data_set->{global}->set($_, $pairs->{$_}) foreach keys %$pairs;
$self->_save_dynamic();
return 1;
}
sub _store_objects {
( run in 2.861 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )