Add ts to Service resource. Rewrite how pt-agent handles applying services: use new meta attrib to find real service, then look for start-service and stop-service; schedule anything with a run_schedule or spool_schedule. Remove Percona/WebAPI/Util.

This commit is contained in:
Daniel Nichter
2013-04-18 16:47:15 -06:00
parent 03f8720871
commit 7cee5a480e
4 changed files with 186 additions and 239 deletions

View File

@@ -27,7 +27,6 @@ BEGIN {
Percona::WebAPI::Resource::Config Percona::WebAPI::Resource::Config
Percona::WebAPI::Resource::Service Percona::WebAPI::Resource::Service
Percona::WebAPI::Resource::Task Percona::WebAPI::Resource::Task
Percona::WebAPI::Util
VersionCheck VersionCheck
DSNParser DSNParser
OptionParser OptionParser
@@ -1350,6 +1349,12 @@ package Percona::WebAPI::Resource::Service;
use Lmo; use Lmo;
has 'ts' => (
is => 'ro',
isa => 'Int',
required => 1,
);
has 'name' => ( has 'name' => (
is => 'ro', is => 'ro',
isa => 'Str', isa => 'Str',
@@ -1374,7 +1379,7 @@ has 'spool_schedule' => (
required => 0, required => 0,
); );
has 'run_once' => ( has 'meta' => (
is => 'ro', is => 'ro',
isa => 'Bool', isa => 'Bool',
required => 0, required => 0,
@@ -1466,44 +1471,6 @@ no Lmo;
# End Percona::WebAPI::Resource::Task package # End Percona::WebAPI::Resource::Task package
# ########################################################################### # ###########################################################################
# ###########################################################################
# Percona::WebAPI::Util package
# This package is a copy without comments from the original. The original
# with comments and its test file can be found in the Bazaar repository at,
# lib/Percona/WebAPI/Util.pm
# t/lib/Percona/WebAPI/Util.t
# See https://launchpad.net/percona-toolkit for more information.
# ###########################################################################
{
package Percona::WebAPI::Util;
use JSON;
use Digest::MD5 qw(md5_hex);
use Percona::WebAPI::Representation;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = (qw(resource_diff));
sub resource_diff {
my ($x, $y) = @_;
return 0 if !$x && !$y;
return 1 if ($x && !$y) || (!$x && $y);
my $json = JSON->new->canonical([1]); # avoid hash key sort diffs
my $x_hex = md5_hex(
Percona::WebAPI::Representation::as_json($x, json => $json));
my $y_hex = md5_hex(
Percona::WebAPI::Representation::as_json($y, json => $json));
return $x_hex eq $y_hex ? 0 : 1;
}
1;
}
# ###########################################################################
# End Percona::WebAPI::Util package
# ###########################################################################
# ########################################################################### # ###########################################################################
# VersionCheck package # VersionCheck package
# This package is a copy without comments from the original. The original # This package is a copy without comments from the original. The original
@@ -4792,7 +4759,6 @@ use Percona::WebAPI::Representation;
use Percona::WebAPI::Util; use Percona::WebAPI::Util;
Percona::Toolkit->import(qw(_d Dumper have_required_args)); Percona::Toolkit->import(qw(_d Dumper have_required_args));
Percona::WebAPI::Util->import(qw(resource_diff));
Percona::WebAPI::Representation->import(qw(as_json as_config)); Percona::WebAPI::Representation->import(qw(as_json as_config));
Transformers->import(qw(ts)); Transformers->import(qw(ts));
@@ -5341,7 +5307,7 @@ sub run_agent {
my $success; my $success;
my $new_daemon; my $new_daemon;
my $config; my $config;
my $services; my $services = {};
while ( $_oktorun->() ) { while ( $_oktorun->() ) {
($config, $lib_dir, $new_daemon, $success) = get_config( ($config, $lib_dir, $new_daemon, $success) = get_config(
link => $agent->links->{config}, link => $agent->links->{config},
@@ -5632,7 +5598,7 @@ sub get_services {
my $lib_dir = $args{lib_dir}; my $lib_dir = $args{lib_dir};
# Optional args # Optional args
my $services = $args{services}; # may not be defined yet my $prev_services = $args{services}; # may not be defined yet
my $success = 0; my $success = 0;
@@ -5640,40 +5606,66 @@ sub get_services {
_info('Getting services'); _info('Getting services');
# Get services from Percona. # Get services from Percona.
my $new_services = $client->get( my $curr_services = $client->get(
link => $link, link => $link,
); );
# If the current and new services are different, # If the current and new services are different,
# write the new ones to disk, then schedule them. # write the new ones to disk, then schedule them.
if ( resource_diff($services, $new_services) ) { if ( $curr_services && @$curr_services ) {
_info('New services');
write_services( # Determine which services are new (added), changed/updated,
services => $new_services, # and removed.
lib_dir => $lib_dir, my $sorted_services = sort_services(
json => $args{json}, # optional, for testing prev_services => $prev_services,
curr_services => $curr_services,
); );
# First, save each service in --lib/services/. This must
# be done before calling start_services() because that sub
# looks for --lib/services/start-service, etc.
write_services(
sorted_services => $sorted_services,
lib_dir => $lib_dir,
json => $args{json}, # optional, for testing
);
# Start and restart services. This must be done before calling
# schedule_services() so that, for example, start-query-history
# is ran before query-history is scheduled and starts running.
# Start new services.
# TODO: this probably can't/won't fail, but if it does, is it # TODO: this probably can't/won't fail, but if it does, is it
# worth setting success=0? # worth setting success=0?
start_services( start_services(
services => $new_services, services => $sorted_services->{added},
lib_dir => $lib_dir, lib_dir => $lib_dir,
bin_dir => $args{bin_dir}, # optional, for testing bin_dir => $args{bin_dir}, # optional, for testing
); );
# Restart existing, re-configured services.
start_services(
restart => 1,
services => $sorted_services->{updated},
lib_dir => $lib_dir,
bin_dir => $args{bin_dir}, # optional, for testing
);
# Schedule any services with a run_schedule or spool_schedule.
# This must be called last, after write_services() and
# start_services() because, for example, a service schedule
# to run at */5 may run effectively immediate if we write
# the new crontab at 00:04:59, so everything has to be
# ready to go at this point.
schedule_services( schedule_services(
services => $new_services, services => $sorted_services->{services},
lib_dir => $lib_dir, lib_dir => $lib_dir,
bin_dir => $args{bin_dir}, # optional, for testing bin_dir => $args{bin_dir}, # optional, for testing
); );
$services = $new_services; $prev_services = $sorted_services->{services};
$success = 1; $success = 1;
_info('Services applied successfully');
_info('Services updated successfully: '
. join(', ', map { $_->name } @$services));
} }
else { else {
_info('Services have not changed'); _info('Services have not changed');
@@ -5683,7 +5675,47 @@ sub get_services {
_warn($EVAL_ERROR); _warn($EVAL_ERROR);
} }
return ($services, $success); return $prev_services, $success;
}
sub sort_services {
my (%args) = @_;
have_required_args(\%args, qw(
prev_services
curr_services
)) or die;
my $prev_services = $args{prev_services}; # hashref
my $curr_services = $args{curr_services}; # arrayref
my $services; # curr_services as hashref keyed on service name
my %have_service; # names of current services
my @added;
my @updated;
my @removed;
foreach my $service ( @$curr_services ) {
my $name = $service->name;
$services->{$name} = $service;
if ( !exists $prev_services->{$name} ) {
push @added, $service;
}
elsif ( $service->ts > $prev_services->{$name}->ts ) {
push @updated, $service;
}
}
if ( scalar keys %$prev_services ) {
@removed = grep { !exists $services->{$_->name} } values %$prev_services;
}
my $sorted_services = {
services => $services,
added => \@added,
updated => \@updated,
removed => \@removed,
};
return $sorted_services;
} }
# Write each service to its own file in --lib/. Remove services # Write each service to its own file in --lib/. Remove services
@@ -5692,11 +5724,11 @@ sub write_services {
my (%args) = @_; my (%args) = @_;
have_required_args(\%args, qw( have_required_args(\%args, qw(
services sorted_services
lib_dir lib_dir
)) or die; )) or die;
my $services = $args{services}; my $sorted_services = $args{sorted_services};
my $lib_dir = $args{lib_dir}; my $lib_dir = $args{lib_dir};
# Optional args # Optional args
my $json = $args{json}; # for testing my $json = $args{json}; # for testing
@@ -5705,11 +5737,12 @@ sub write_services {
_info("Writing services to $lib_dir"); _info("Writing services to $lib_dir");
# Write every current service. # Save current, active services.
my %have_service; foreach my $service (
foreach my $service ( @$services ) { @{$sorted_services->{added}}, @{$sorted_services->{updated}}
) {
my $file = $lib_dir . '/' . $service->name; my $file = $lib_dir . '/' . $service->name;
my $action = -f $file ? 'Updated' : 'Created'; my $action = -f $file ? 'Updated' : 'Added';
open my $fh, '>', $file open my $fh, '>', $file
or die "Error opening $file: $OS_ERROR"; or die "Error opening $file: $OS_ERROR";
print { $fh } as_json($service, with_links => 1, json => $json) print { $fh } as_json($service, with_links => 1, json => $json)
@@ -5717,29 +5750,23 @@ sub write_services {
close $fh close $fh
or die "Error closing $file: $OS_ERROR"; or die "Error closing $file: $OS_ERROR";
_info("$action $file"); _info("$action $file");
$have_service{$service->name} = $service;
} }
# Remove old services: one's that still exisit but weren't # Remove old services.
# writen ^, so they're no longer implemented. foreach my $service ( @{$sorted_services->{removed}} ) {
opendir(my $dh, $lib_dir) my $file = $lib_dir . '/' . $service->name;
or die "Error opening $lib_dir: $OS_ERROR"; if ( -f $file ) {
while ( my $file = readdir($dh) ) { unlink $file
next if -d $file;
if ( !$have_service{$file} ) {
unlink "$lib_dir/$file"
or die "Error removing $file: $OS_ERROR"; or die "Error removing $file: $OS_ERROR";
_info("Removed $file"); _info("Removed $file");
} }
} }
closedir $dh
or die "Error closing $lib_dir: $OS_ERROR";
return; return;
} }
# Write Service->run_schedule and (optionally) Service->spool_schedule # Write Service->run_schedule and Service->spool_schedule lines to crontab,
# lines to crontab, along with any other non-pt-agent lines, and load. # along with any other non-pt-agent lines, then reload crontab.
sub schedule_services { sub schedule_services {
my (%args) = @_; my (%args) = @_;
@@ -5757,7 +5784,9 @@ sub schedule_services {
# Only schedule "periodic" services, i.e. ones that run periodically, # Only schedule "periodic" services, i.e. ones that run periodically,
# not just once. # not just once.
my @periodic_services = grep { !$_->run_once; } @$services; my @periodic_services = grep { $_->run_schedule || $_->spool_schedule }
sort { $a->name cmp $b->name }
values %$services;
my $new_crontab = make_new_crontab( my $new_crontab = make_new_crontab(
%args, %args,
@@ -5809,11 +5838,13 @@ sub make_new_crontab {
my @pt_agent_lines; my @pt_agent_lines;
foreach my $service ( @$services ) { foreach my $service ( @$services ) {
push @pt_agent_lines, if ( $service->run_schedule ) {
$service->run_schedule push @pt_agent_lines,
. ($env_vars ? " $env_vars" : '') $service->run_schedule
. " ${bin_dir}pt-agent --run-service " . ($env_vars ? " $env_vars" : '')
. $service->name; . " ${bin_dir}pt-agent --run-service "
. $service->name;
}
if ( $service->spool_schedule ) { if ( $service->spool_schedule ) {
push @pt_agent_lines, push @pt_agent_lines,
$service->spool_schedule $service->spool_schedule
@@ -5829,9 +5860,12 @@ sub make_new_crontab {
return $new_crontab; return $new_crontab;
} }
# Start all services that have the run_once_on_start flag enabled. This is # Start real services, i.e. non-meta services. A real service is like
# used for "setup services" so the user doesn't have to wait until the next # "query-history", which probably has meta-services like "start-query-history"
# the service is actually scheduled to run. # and "stop-query-history". We infer these start/stop meta-services
# from the real service's name. A service doesn't require meta-services;
# there may be nothing to do to start it, in which case the real service
# starts running due to its run_schedule and schedule_services().
sub start_services { sub start_services {
my (%args) = @_; my (%args) = @_;
have_required_args(\%args, qw( have_required_args(\%args, qw(
@@ -5842,48 +5876,68 @@ sub start_services {
my $lib_dir = $args{lib_dir}; my $lib_dir = $args{lib_dir};
# Optional args # Optional args
my $bin_dir = defined $args{bin_dir} ? $args{bin_dir} my $restart = $args{restart};
: "$FindBin::Bin/"; my $bin_dir = defined $args{bin_dir} ? $args{bin_dir}
: "$FindBin::Bin/";
my $exec_cmd = $args{exec_cmd} || sub { return system(@_) };
my $env_vars = env_vars(); my $env_vars = env_vars();
my $log = "$lib_dir/logs/start.log";
# Remove old meta files. my $cmd_fmt = ($env_vars ? "$env_vars " : '')
my $meta_files = "$lib_dir/meta/*"; . $bin_dir . "pt-agent --run-service %s >> $log 2>&1";
foreach my $meta_file ( glob $meta_files ) {
if ( unlink $meta_file ) {
_info("Removed $meta_file");
}
else {
_warn("Cannot remove $meta_file: $OS_ERROR");
}
}
SERVICE: SERVICE:
foreach my $service ( @$services ) { foreach my $service ( @$services ) {
next unless $service->run_once; next if $service->meta; # only real services
# Start the service and wait for it to exit. Log its initial my $name = $service->name;
# output to a special log file. If it dies early, this log
# file will contain the reason. Else, if it starts, it will # To restart, one must first stop, then start afterwards.
# switch to its default log file ending in ".run". if ( $restart ) {
my $start_log = "$lib_dir/logs/" . $service->name . ".start"; if ( -f "$lib_dir/services/stop-$name" ) {
my $cmd = ($env_vars ? "$env_vars " : '') my $cmd = sprintf $cmd_fmt, "stop-$name";
. "${bin_dir}pt-agent --run-service " . $service->name _info("Stopping $name: $cmd");
. " </dev/null" $exec_cmd->($cmd);
. " >$start_log 2>&1"; my $cmd_exit_status = $CHILD_ERROR >> 8;
_info('Starting ' . $service->name . ' service: ' . $cmd); if ( $cmd_exit_status != 0 ) {
system($cmd); _warn("Error stopping $name, check $log and "
my $cmd_exit_status = $CHILD_ERROR >> 8; . "$lib_dir/logs/$name.run");
if ( $cmd_exit_status != 0 ) { next SERVICE;
my $err = slurp($start_log); }
_warn('Error starting ' . $service->name . ': ' . ($err || '')); }
next SERVICE;
} }
unlink $start_log # Remove old meta files. Meta files are generally temporary
or _warn("Cannot remove $start_log: $OS_ERROR"); # in any case, persisting info from one interval to the next.
# If the service has changed (e.g., report interval is longer),
# there's no easy way to tranistion from old metadata to new,
# so we just rm the old metadata and start anew.
my $meta_files = "$lib_dir/meta/$name*";
foreach my $meta_file ( glob $meta_files ) {
if ( unlink $meta_file ) {
_info("Removed $meta_file");
}
else {
_warn("Cannot remove $meta_file: $OS_ERROR");
}
}
_info($service->name . ' has started'); # Start the service and wait for it to exit. If it dies
# really early (before it really begins), our log file will
# have the error; else, the service should automatically
# switch to its default log file ending in ".run".
if ( -f "$lib_dir/services/start-$name" ) {
my $cmd = sprintf $cmd_fmt, "start-$name";
_info("Starting $name: $cmd");
$exec_cmd->($cmd);
my $cmd_exit_status = $CHILD_ERROR >> 8;
if ( $cmd_exit_status != 0 ) {
_warn("Error starting $name, check $log and "
."$lib_dir/logs/$name.run");
next SERVICE;
}
_info("Started $name successfully");
}
} }
return; return;
@@ -5908,8 +5962,9 @@ sub run_service {
my $cxn = $args{Cxn}; my $cxn = $args{Cxn};
# Optional args # Optional args
my $json = $args{json}; my $json = $args{json}; # for testing
my $curr_ts = $args{curr_ts} || ts(time, 1); # 1=UTC my $suffix = $args{suffix} || '.' . int(time); # for testing
my $curr_ts = $args{curr_ts} || ts(time, 1); # 1=UTC
# The seconds are :01 or :02 because cron runs commands about 1 or 2 # The seconds are :01 or :02 because cron runs commands about 1 or 2
# seconds after the minute mark. Unfortunately this means that interval # seconds after the minute mark. Unfortunately this means that interval
@@ -6021,8 +6076,7 @@ sub run_service {
# Run the tasks, spool any data. # Run the tasks, spool any data.
my @output_files; my @output_files;
my $data_file = $service->name my $data_file = $service->name . ($service->run_once ? '' : $suffix);
. ($service->run_once ? '' : '.' . int(time));
my $tmp_data_file = "$tmp_dir/$data_file"; my $tmp_data_file = "$tmp_dir/$data_file";
my $have_data_file = 0; my $have_data_file = 0;
my $taskno = 0; my $taskno = 0;
@@ -6541,7 +6595,7 @@ sub stop_agent {
_info("Removing all services from crontab..."); _info("Removing all services from crontab...");
eval { eval {
schedule_services( schedule_services(
services => [], services => {},
lib_dir => $lib_dir, lib_dir => $lib_dir,
quiet => 1, quiet => 1,
); );

View File

@@ -22,6 +22,12 @@ package Percona::WebAPI::Resource::Service;
use Lmo; use Lmo;
has 'ts' => (
is => 'ro',
isa => 'Int',
required => 1,
);
has 'name' => ( has 'name' => (
is => 'ro', is => 'ro',
isa => 'Str', isa => 'Str',
@@ -46,7 +52,7 @@ has 'spool_schedule' => (
required => 0, required => 0,
); );
has 'run_once' => ( has 'meta' => (
is => 'ro', is => 'ro',
isa => 'Bool', isa => 'Bool',
required => 0, required => 0,

View File

@@ -1,48 +0,0 @@
# This program is copyright 2012-2013 Percona Inc.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2; OR the Perl Artistic License. On UNIX and similar
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
# licenses.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA 02111-1307 USA.
# ###########################################################################
# Percona::WebAPI::Util package
# ###########################################################################
{
package Percona::WebAPI::Util;
use JSON;
use Digest::MD5 qw(md5_hex);
use Percona::WebAPI::Representation;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = (qw(resource_diff));
sub resource_diff {
my ($x, $y) = @_;
return 0 if !$x && !$y;
return 1 if ($x && !$y) || (!$x && $y);
my $json = JSON->new->canonical([1]); # avoid hash key sort diffs
my $x_hex = md5_hex(
Percona::WebAPI::Representation::as_json($x, json => $json));
my $y_hex = md5_hex(
Percona::WebAPI::Representation::as_json($y, json => $json));
return $x_hex eq $y_hex ? 0 : 1;
}
1;
}
# ###########################################################################
# End Percona::WebAPI::Util package
# ###########################################################################

View File

@@ -1,65 +0,0 @@
#!/usr/bin/perl
BEGIN {
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
};
use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use Test::More;
use PerconaTest;
use Percona::Toolkit;
use Percona::WebAPI::Resource::Config;
use Percona::WebAPI::Util qw(resource_diff);
my $x = Percona::WebAPI::Resource::Config->new(
ts => '100',
name => 'Default',
options => {
'lib' => '/var/lib',
'spool' => '/var/spool',
},
);
my $y = Percona::WebAPI::Resource::Config->new(
ts => '100',
name => 'Default',
options => {
'lib' => '/var/lib',
'spool' => '/var/spool',
},
);
is(
resource_diff($x, $y),
0,
"No diff"
);
$y->options->{spool} = '/var/lib/spool';
is(
resource_diff($x, $y),
1,
"Big diff in 1 attrib"
);
# Restore this...
$y->options->{spool} = '/var/spool';
# Change this...
$y->options->{ts} = '101';
is(
resource_diff($x, $y),
1,
"Small diff in 1 attrib"
);
# #############################################################################
# Done.
# #############################################################################
done_testing;