2751 lines
79 KiB
Perl
2751 lines
79 KiB
Perl
|
#!/usr/bin/perl
|
||
|
##############################################################################
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
moveuclusterd - User-mover task coordinater daemon
|
||
|
|
||
|
=head1 SYNOPSIS
|
||
|
|
||
|
$ moveuclusterd OPTIONS
|
||
|
|
||
|
=head2 OPTIONS
|
||
|
|
||
|
=over 4
|
||
|
|
||
|
=item -d, --debug
|
||
|
|
||
|
Output debugging information in addition to normal progress messages. May be
|
||
|
specified more than once to increase debug level.
|
||
|
|
||
|
=item -D, --daemon
|
||
|
|
||
|
Background the program.
|
||
|
|
||
|
=item -h, --help
|
||
|
|
||
|
Output a help message and exit.
|
||
|
|
||
|
=item -H, --host=HOST
|
||
|
|
||
|
Listen on the specified I<HOST> instead of the default '0.0.0.0'.
|
||
|
|
||
|
=item -m, --maxlocktime=SECONDS
|
||
|
|
||
|
Set the number of seconds that is targeted as the timespan to keep jobs locked
|
||
|
before assigning them. If the oldest job in a cluster's queue is older than this
|
||
|
value (120 by default), no users will be locked for that queue until the next
|
||
|
check.
|
||
|
|
||
|
=item -p, --port=PORT
|
||
|
|
||
|
Listen to the given I<PORT> instead of the default 2789.
|
||
|
|
||
|
=item -r, --defaultrate=INTEGER
|
||
|
|
||
|
Set the default rate limit for any source cluster which has not had its rate set
|
||
|
to I<INTEGER>. The default rate is 1.
|
||
|
|
||
|
=item -s, --lockscale=INTEGER
|
||
|
|
||
|
Set the lock-scaling factor to I<INTEGER>. The lock scaling factor is used to
|
||
|
decide how many users to lock per source cluster; a scaling factor of C<3> (the
|
||
|
default) would cause the jobserver to try to maintain 3 x the number of jobs as
|
||
|
there are allowed connections for a given cluster, modulo the C<maxlocktime>.
|
||
|
|
||
|
=item -v, --verbose
|
||
|
|
||
|
Output the jobserver's log to STDERR.
|
||
|
|
||
|
=back
|
||
|
|
||
|
=head1 REQUIRES
|
||
|
|
||
|
I<Token requires line>
|
||
|
|
||
|
=head1 DESCRIPTION
|
||
|
|
||
|
None yet.
|
||
|
|
||
|
=head1 AUTHOR
|
||
|
|
||
|
Michael Granger E<lt>ged@danga.comE<gt>
|
||
|
|
||
|
Copyright (c) 2004 Danga Interactive. All rights reserved.
|
||
|
|
||
|
This module is free software. You may use, modify, and/or redistribute this
|
||
|
software under the terms of the Perl Artistic License. (See
|
||
|
http://language.perl.com/misc/Artistic.html)
|
||
|
|
||
|
=cut
|
||
|
|
||
|
##############################################################################
|
||
|
package moveuclusterd;
|
||
|
use strict;
|
||
|
use warnings qw{all};
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
### I N I T I A L I Z A T I O N
|
||
|
###############################################################################
|
||
|
BEGIN {
|
||
|
|
||
|
# Turn STDOUT buffering off
|
||
|
$| = 1;
|
||
|
|
||
|
# Versioning stuff and custom includes
|
||
|
use vars qw{$VERSION $RCSID};
|
||
|
$VERSION = do { my @r = (q$Revision: 1.20 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
|
||
|
$RCSID = q$Id: moveuclusterd.pl,v 1.20 2004/11/12 00:31:07 deveiant Exp $;
|
||
|
|
||
|
# Define some constants
|
||
|
use constant TRUE => 1;
|
||
|
use constant FALSE => 0;
|
||
|
|
||
|
use lib "$ENV{LJHOME}/cgi-bin";
|
||
|
require "ljlib.pl";
|
||
|
|
||
|
# Modules
|
||
|
use Carp qw{croak confess};
|
||
|
use Getopt::Long qw{GetOptions};
|
||
|
use Pod::Usage qw{pod2usage};
|
||
|
|
||
|
Getopt::Long::Configure( 'bundling' );
|
||
|
}
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
### C O N F I G U R A T I O N G L O B A L S
|
||
|
###############################################################################
|
||
|
|
||
|
### Main body
|
||
|
sub MAIN {
|
||
|
my (
|
||
|
$debugLevel, # Debugging level to set in server
|
||
|
$helpFlag, # User requested help?
|
||
|
$daemonFlag, # Background after starting?
|
||
|
$defaultRate, # Default src cluster rate cmdline setting
|
||
|
$verboseFlag, # Output the log or no?
|
||
|
$server, # JobServer object
|
||
|
%config, # JobServer configuration
|
||
|
$port, # Port to listen on
|
||
|
$host, # Address to listen on
|
||
|
$lockScale, # Lock scaling factor
|
||
|
$maxLockTime, # Max time to keep users locked
|
||
|
);
|
||
|
|
||
|
# Print the program header and read in command line options
|
||
|
GetOptions(
|
||
|
'D|daemon' => \$daemonFlag,
|
||
|
'H|host=s' => \$host,
|
||
|
'd|debug+' => \$debugLevel,
|
||
|
'h|help' => \$helpFlag,
|
||
|
'm|maxlocktime=i' => \$maxLockTime,
|
||
|
'p|port=i' => \$port,
|
||
|
'r|defaultrate=i' => \$defaultRate,
|
||
|
's|lockscale=i' => \$lockScale,
|
||
|
'v|verbose' => \$verboseFlag,
|
||
|
) or abortWithUsage();
|
||
|
|
||
|
# If the -h flag was given, just show the usage and quit
|
||
|
helpMode() and exit if $helpFlag;
|
||
|
|
||
|
# Build the configuration hash
|
||
|
$config{host} = $host if $host;
|
||
|
$config{port} = $port if $port;
|
||
|
$config{daemon} = $daemonFlag;
|
||
|
$config{debugLevel} = $debugLevel || 0;
|
||
|
$config{defaultRate} = $defaultRate if $defaultRate;
|
||
|
$config{lockScale} = $lockScale if $lockScale;
|
||
|
$config{maxLockTime} = $maxLockTime if defined $maxLockTime;
|
||
|
|
||
|
# Create a new daemon object
|
||
|
$server = new JobServer ( %config );
|
||
|
|
||
|
# Add a simple log handler if they've requested verbose output
|
||
|
if ( $verboseFlag ) {
|
||
|
my $tmplogger = sub {
|
||
|
my ( $level, $msg ) = @_;
|
||
|
print STDERR "[$level] $msg\n";
|
||
|
};
|
||
|
$server->addHandler( 'log', 'verboselogger', $tmplogger );
|
||
|
}
|
||
|
|
||
|
# Start the server
|
||
|
$server->start();
|
||
|
}
|
||
|
|
||
|
|
||
|
### FUNCTION: helpMode()
|
||
|
### Exit normally after printing the usage message
|
||
|
sub helpMode {
|
||
|
pod2usage( -verbose => 1, -exitval => 0 );
|
||
|
}
|
||
|
|
||
|
|
||
|
### FUNCTION: abortWithUsage( $message )
|
||
|
### Abort the program showing usage message.
|
||
|
sub abortWithUsage {
|
||
|
my $msg = @_ ? join('', @_) : "";
|
||
|
|
||
|
if ( $msg ) {
|
||
|
pod2usage( -verbose => 1, -exitval => 1, -message => "$msg" );
|
||
|
} else {
|
||
|
pod2usage( -verbose => 1, -exitval => 1 );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### If run from the command line, run the server.
|
||
|
if ( $0 eq __FILE__ ) { MAIN() }
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### T I M E D B U F F E R C L A S S
|
||
|
#####################################################################
|
||
|
package TimedBuffer;
|
||
|
|
||
|
BEGIN {
|
||
|
use Carp qw{croak confess};
|
||
|
}
|
||
|
|
||
|
our $DefaultExpiration = 120;
|
||
|
|
||
|
### (CONSTRUCTOR) METHOD: new( $seconds )
|
||
|
### Create a new timed buffer which will remove entries the specified number of
|
||
|
### I<seconds> after being added.
|
||
|
sub new {
|
||
|
my $proto = shift;
|
||
|
my $class = ref $proto || $proto;
|
||
|
my $seconds = shift || $DefaultExpiration;
|
||
|
|
||
|
my $self = bless {
|
||
|
buffer => [],
|
||
|
seconds => $seconds,
|
||
|
}, $class;
|
||
|
|
||
|
return $self;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: add( @items )
|
||
|
### Add the given I<items> to the buffer, shifting off older ones if they are
|
||
|
### expired.
|
||
|
sub add {
|
||
|
my $self = shift or confess "Cannot be used as a function";
|
||
|
my @items = @_;
|
||
|
|
||
|
my $expiration = time - $self->{seconds};
|
||
|
my $buffer = $self->{buffer};
|
||
|
|
||
|
# Expire old entries and add the new ones
|
||
|
@$buffer = grep { $_->[1] > $expiration } @$buffer;
|
||
|
push @$buffer, map {[ $_, time ]} @items;
|
||
|
|
||
|
return scalar @$buffer;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: get( [@indices] )
|
||
|
### Return the items in the buffer at the specified I<indices>, or all items in
|
||
|
### the buffer if no I<indices> are given.
|
||
|
sub get {
|
||
|
my $self = shift or confess "Cannot be used as a function";
|
||
|
|
||
|
my $expiration = time - $self->{seconds};
|
||
|
my $buffer = $self->{buffer};
|
||
|
|
||
|
# Expire old entries
|
||
|
@$buffer = grep { $_->[1] > $expiration } @$buffer;
|
||
|
|
||
|
# Return just the values from the buffer, either in a slice if they
|
||
|
# specified indexes, or the whole thing if not.
|
||
|
if ( @_ ) {
|
||
|
return map { $_->[0] } @{$buffer}[ @_ ];
|
||
|
} else {
|
||
|
return map { $_->[0] } @$buffer;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### D A E M O N C L A S S
|
||
|
#####################################################################
|
||
|
package JobServer;
|
||
|
|
||
|
BEGIN {
|
||
|
use IO::Socket qw{};
|
||
|
use Data::Dumper qw{Dumper};
|
||
|
use Carp qw{croak confess};
|
||
|
use Time::HiRes qw{gettimeofday tv_interval};
|
||
|
use POSIX qw{};
|
||
|
|
||
|
use fields (
|
||
|
'clients', # Connected client objects
|
||
|
'config', # Configuration hash
|
||
|
'listener', # The listener socket
|
||
|
'handlers', # Client event handlers
|
||
|
'jobs', # Mover jobs
|
||
|
'totaljobs', # Count of jobs processed
|
||
|
'assignments', # Jobs that have been assigned
|
||
|
'users', # Users in the queue
|
||
|
'ratelimits', # Cached cluster ratelimits
|
||
|
'raterules', # Rules for building ratelimit table
|
||
|
'jobcounts', # Counts per cluster of running jobs
|
||
|
'starttime', # Server startup epoch time
|
||
|
'recentmoves', # Timed buffer of recently-completed jobs
|
||
|
);
|
||
|
|
||
|
use lib "$ENV{LJHOME}/cgi-bin";
|
||
|
require 'ljlib.pl';
|
||
|
|
||
|
use base qw{fields};
|
||
|
}
|
||
|
|
||
|
|
||
|
### Class globals
|
||
|
|
||
|
# Default configuration
|
||
|
our ( %DefaultConfig, %LogLevels );
|
||
|
|
||
|
INIT {
|
||
|
|
||
|
# Default server configuration; this is merged with any config args the user
|
||
|
# specifies in the call to the constructor. Most of these correspond with
|
||
|
# command-line flags, so see that section of the POD header for more
|
||
|
# information.
|
||
|
%DefaultConfig = (
|
||
|
port => 2789, # Port to listen on
|
||
|
host => '0.0.0.0', # Host to bind to
|
||
|
listenQueue => 5, # Listen queue depth
|
||
|
daemon => 0, # Daemonize or not?
|
||
|
debugLevel => 0, # Debugging log level
|
||
|
defaultRate => 1, # The default src cluster rate
|
||
|
lockScale => 3, # Scaling factor for locking users
|
||
|
maxLockTime => 120, # Max seconds to keep users locked
|
||
|
);
|
||
|
|
||
|
my $level = 0;
|
||
|
%LogLevels = map {
|
||
|
$_ => $level++,
|
||
|
} qw{debug info notice warn crit fatal};
|
||
|
|
||
|
|
||
|
$Data::Dumper::Terse = 1;
|
||
|
$Data::Dumper::Indent = 1;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# Datastructures of class members:
|
||
|
#
|
||
|
# clients: Hashref of connected clients, keyed by fdno
|
||
|
#
|
||
|
# jobs: A hash of arrays of JobServer::Job objects:
|
||
|
# {
|
||
|
# <srcclusterid> => [ $job1, $job2, ... ],
|
||
|
# ...
|
||
|
# }
|
||
|
#
|
||
|
# users: A hash index into the inner arrays of 'jobs', keyed by
|
||
|
# userid.
|
||
|
#
|
||
|
# assignments: A hash of arrays; when a job is assigned to a mover, the
|
||
|
# corresponding JobServer::Job is moved into this hash,
|
||
|
# keyed by the fdno of the mover responsible.
|
||
|
#
|
||
|
# handlers: Hash of hashes; this is used to register callbacks for clients that
|
||
|
# want to monitor the server, receiving log or debugging messages,
|
||
|
# new job notifications, etc.
|
||
|
#
|
||
|
# totaljobs: Count of total jobs added to the daemon.
|
||
|
#
|
||
|
# raterules: Maximum number of jobs which can be run against source clusters,
|
||
|
# keyed by clusterid. If a global rate limit has been set, this
|
||
|
# hash also contains a special key 'global' to contain it.
|
||
|
#
|
||
|
# ratelimits: Cached ratelimits for clusters -- this is rebuilt whenever a
|
||
|
# ratelimit rule is added, and is partially rebuilt when new jobs
|
||
|
# are added.
|
||
|
#
|
||
|
# jobcounts: Count of jobs running against source clusters, keyed by
|
||
|
# source clusterid.
|
||
|
|
||
|
### (CONSTRUCTOR) METHOD: new( %config )
|
||
|
### Create a new JobServer object with the given I<config>.
|
||
|
sub new {
|
||
|
my JobServer $self = shift;
|
||
|
my %config = @_;
|
||
|
|
||
|
$self = fields::new( $self ) unless ref $self;
|
||
|
|
||
|
# Client and job queues
|
||
|
$self->{clients} = {}; # fd => client obj
|
||
|
$self->{jobs} = {}; # pending jobs: srcluster => [ jobs ]
|
||
|
$self->{users} = {}; # by-userid hash of jobs
|
||
|
$self->{assignments} = {}; # fd => job object
|
||
|
$self->{totaljobs} = 0; # Count of total jobs added
|
||
|
$self->{raterules} = {}; # User-set rate-limit rules
|
||
|
$self->{ratelimits} = {}; # Cached rate limits by srcclusterid
|
||
|
$self->{jobcounts} = {}; # Count of jobs by srcclusterid
|
||
|
|
||
|
# Create a timed buffer to contain the jobs which have completed in the last
|
||
|
# 6 minutes.
|
||
|
$self->{recentmoves} = new TimedBuffer 360;
|
||
|
|
||
|
# Merge the user-specified configuration with the defaults, with the user's
|
||
|
# overriding.
|
||
|
$self->{config} = {
|
||
|
%DefaultConfig,
|
||
|
%config,
|
||
|
}; # merge
|
||
|
|
||
|
# These two get set by start()
|
||
|
$self->{listener} = undef;
|
||
|
$self->{starttime} = undef;
|
||
|
|
||
|
# CODE refs for handling various events. Keyed by event name, each subhash
|
||
|
# contains registrations for event callbacks. Each subhash is keyed by the
|
||
|
# fdno of the client that requested it, or an arbitrary string if the
|
||
|
# handler belongs to something other than a client.
|
||
|
$self->{handlers} = {
|
||
|
debug => {},
|
||
|
log => {},
|
||
|
};
|
||
|
|
||
|
return $self;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### METHOD: start()
|
||
|
### Start the event loop.
|
||
|
sub start {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
# Start the listener socket
|
||
|
my $listener = new IO::Socket::INET
|
||
|
Proto => 'tcp',
|
||
|
LocalAddr => $self->{config}{host},
|
||
|
LocalPort => $self->{config}{port},
|
||
|
Listen => $self->{config}{listenQueue},
|
||
|
ReuseAddr => 1,
|
||
|
Blocking => 0
|
||
|
or die "new socket: $!";
|
||
|
|
||
|
# Log the server startup, then daemonize if it's called for
|
||
|
$self->logMsg( 'notice', "Server listening on %s:%d\n",
|
||
|
$listener->sockhost, $listener->sockport );
|
||
|
$self->{listener} = $listener;
|
||
|
$self->daemonize if $self->{config}{daemon};
|
||
|
|
||
|
# Remember the startup time
|
||
|
$self->{starttime} = time;
|
||
|
|
||
|
# I don't understand this design -- the Client class is where the event loop
|
||
|
# is? Weird. Thanks to SPUD, though, for the example code.
|
||
|
JobServer::Client->OtherFds( $listener->fileno => sub {$self->createClient} );
|
||
|
JobServer::Client->EventLoop();
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### METHOD: createClient( undef )
|
||
|
### Listener socket readable callback. Accepts a new client socket and wraps a
|
||
|
### JobServer::Client around it.
|
||
|
sub createClient {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
my (
|
||
|
$csock, # Client socket
|
||
|
$client, # JobServer::Client object
|
||
|
$fd, # File descriptor for client
|
||
|
);
|
||
|
|
||
|
# Get the client socket and set it nonblocking
|
||
|
$csock = $self->{listener}->accept or return;
|
||
|
$csock->blocking(0);
|
||
|
$fd = fileno( $csock );
|
||
|
|
||
|
$self->logMsg( 'info', 'Client %d connect: %s:%d',
|
||
|
$fd, $csock->peerhost, $csock->peerport );
|
||
|
|
||
|
# Wrap a client object around it, tell it to watch for input, and send the
|
||
|
# greeting.
|
||
|
$client = JobServer::Client->new( $self, $csock );
|
||
|
$client->watch_read( 1 );
|
||
|
$client->write( "Ready.\r\n" );
|
||
|
|
||
|
return $self->{clients}{$fd} = $client;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: disconnectClient( $client=JobServer::Client[, $requeue] )
|
||
|
### Disconnect the specified I<client> from the server. If I<requeue> is true,
|
||
|
### the job belonging to the client (if any) will be put back into the queue of
|
||
|
### pending jobs.
|
||
|
sub disconnectClient {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $requeue ) = @_;
|
||
|
|
||
|
my (
|
||
|
$csock, # Client socket
|
||
|
$fd, # Client's fdno
|
||
|
$job, # Job that client was working on
|
||
|
);
|
||
|
|
||
|
# Stop further input from the socket
|
||
|
$csock = $client->sock;
|
||
|
$csock->shutdown( 0 ) if $csock->connected;
|
||
|
$fd = fileno( $csock );
|
||
|
$self->logMsg( 'info', "Client %d disconnect: %s:%d",
|
||
|
$fd, $csock->peerhost, $csock->peerport );
|
||
|
|
||
|
# Remove any event handlers registered for the client
|
||
|
$self->removeHandlerFromAll( $fd );
|
||
|
$self->unassignJobForClient( $fd );
|
||
|
|
||
|
# Remove the client from our list
|
||
|
delete $self->{clients}{ $fd };
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: clients( undef )
|
||
|
### Get the list of clients (JobServer::Client objects) currently connected to
|
||
|
### the server.
|
||
|
sub clients {
|
||
|
my JobServer $self = shift;
|
||
|
return values %{$self->{clients}};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: raterules( undef )
|
||
|
### Get the hash of rate rules the server uses to calculate a cluster's
|
||
|
### maximum number of clients.
|
||
|
sub raterules {
|
||
|
my JobServer $self = shift;
|
||
|
return %{$self->{raterules}};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: recentmoves( undef )
|
||
|
### Get the JobServer::Job objects in the server's "recently-moved" timedbuffer.
|
||
|
sub recentmoves {
|
||
|
my JobServer $self = shift;
|
||
|
return $self->{recentmoves}->get;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: defaultRate( undef )
|
||
|
### Get the default cluster rate as set on the command line.
|
||
|
sub defaultRate {
|
||
|
my JobServer $self = shift;
|
||
|
return $self->{config}->{defaultRate};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: addJobs( @jobs=JobServer::Job )
|
||
|
### Add a job to move the user with the given I<userid> to the cluster with the
|
||
|
### specified I<dstclustid>.
|
||
|
sub addJobs {
|
||
|
my JobServer $self = shift;
|
||
|
my @jobs = @_;
|
||
|
|
||
|
my (
|
||
|
@responses, # Inline responses
|
||
|
$clusterid, # Cluster iterator
|
||
|
$job, # Job object iterator
|
||
|
$userid, # User id for user to move
|
||
|
$newJobCount, # Count of jobs added to the queue
|
||
|
);
|
||
|
|
||
|
$newJobCount = 0;
|
||
|
|
||
|
# Iterate over job specifications
|
||
|
JOB: for ( my $i = 0; $i <= $#jobs; $i++ ) {
|
||
|
$job = $jobs[ $i ];
|
||
|
$self->debugMsg( 5, "Adding job: %s", $job->stringify );
|
||
|
|
||
|
( $userid, $clusterid ) = ( $job->userid, $job->srcclusterid );
|
||
|
|
||
|
# Check to be sure this job isn't already queued or in progress.
|
||
|
if ( $self->{users}{$userid} ) {
|
||
|
$self->debugMsg( 2, "Request for duplicate job %s", $job->stringify );
|
||
|
$responses[$i] = "Duplicate job for userid $userid";
|
||
|
next JOB;
|
||
|
}
|
||
|
|
||
|
# Queue the job and point the user index at it.
|
||
|
$self->{jobs}{$clusterid} ||= [];
|
||
|
push @{$self->{jobs}{$clusterid}}, $job;
|
||
|
$self->{users}{ $userid } = $job;
|
||
|
$self->{jobcounts}{$clusterid} ||= 0;
|
||
|
|
||
|
$responses[$i] = "Added job ". ++$self->{totaljobs};
|
||
|
$newJobCount++;
|
||
|
}
|
||
|
|
||
|
# we might've learned some new clusterids
|
||
|
%{$self->{ratelimits}} = ();
|
||
|
|
||
|
# Scan the task table for users to lock and then send notifications to
|
||
|
# anyone who's waiting on new jobs if there were any added.
|
||
|
$self->prelockSomeUsers;
|
||
|
$self->handleEvent( 'add', $newJobCount ) if $newJobCount;
|
||
|
|
||
|
return @responses;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: prelockSomeUsers( undef )
|
||
|
### Mark some of the users in the queues as read-only so the movers don't need
|
||
|
### to do so before moving. Only marks a portion of each queue so as to not
|
||
|
### inconvenience users.
|
||
|
sub prelockSomeUsers {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
my $start = [gettimeofday()];
|
||
|
|
||
|
my (
|
||
|
$jobcount, # Number of jobs queued for a cluster
|
||
|
$rate, # Rate for the cluster in question
|
||
|
$target, # Number of queued jobs we'd like to be locked
|
||
|
$lockcount, # Number of users locked
|
||
|
$scale, # Lock scaling factor
|
||
|
$maxLockTime, # Max number of seconds to keep users locked
|
||
|
$clients, # Number of currently-connected clients
|
||
|
$jobs, # Job queue per cluster
|
||
|
);
|
||
|
|
||
|
# Twiddle some database bits out in magic voodoo land
|
||
|
LJ::start_request();
|
||
|
|
||
|
# Set the scaling factor -- this is a command-line setting that affects how
|
||
|
# deep the queue is locked per source cluster.
|
||
|
$scale = $self->{config}{lockScale};
|
||
|
$maxLockTime = $self->{config}{maxLockTime};
|
||
|
|
||
|
$self->debugMsg( 3, "Prelocking with scale: $scale, maxlocktime: $maxLockTime" );
|
||
|
|
||
|
# Iterate over all the queues we have by cluster
|
||
|
CLUSTER: foreach my $clusterid ( keys %{$self->{jobs}} ) {
|
||
|
$rate = $self->getClusterRateLimit( $clusterid );
|
||
|
$target = $rate * $scale;
|
||
|
|
||
|
# Now iterate partway into the queue of jobs for the cluster, locking
|
||
|
# some users if there are some that need locking
|
||
|
$jobs = $self->{jobs}{ $clusterid };
|
||
|
JOB: for ( my $i = 0; $i <= $target; $i++ ) {
|
||
|
|
||
|
# If there are fewer jobs than the target number to be locked, or
|
||
|
# the current job is older than the maximum number of seconds to
|
||
|
# keep a user locked, skip to the next cluster
|
||
|
next CLUSTER if $i > $#$jobs;
|
||
|
next CLUSTER if $jobs->[$i]->secondsSinceLock > $maxLockTime;
|
||
|
|
||
|
# Skip jobs that are already prelocked. If locking fails, assume
|
||
|
# there's some database problem and don't try to prelock any more
|
||
|
# until next time.
|
||
|
next JOB if $jobs->[$i]->isPrelocked;
|
||
|
$jobs->[$i]->prelock or last CLUSTER;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$self->debugMsg( 4, "Prelock time: %0.5fs", tv_interval($start) );
|
||
|
return $lockcount;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: getClusterRateLimit( $clusterid )
|
||
|
### Return the number of connections which can be reading from the cluster with
|
||
|
### the given I<clusterid>.
|
||
|
sub getClusterRateLimit {
|
||
|
my JobServer $self = shift;
|
||
|
my $clusterid = shift or confess "No clusterid";
|
||
|
|
||
|
# Swap the next two lines to make the 'global' rate override those of
|
||
|
# specific clusters.
|
||
|
return $self->{raterules}{ $clusterid } if exists $self->{raterules}{ $clusterid };
|
||
|
return $self->{raterules}{global} if exists $self->{raterules}{global};
|
||
|
return $self->{config}{defaultRate};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: getClusterRateLimits( undef )
|
||
|
### Return the rate limits for all known clusters as a hash (or hashref if
|
||
|
### called in scalar context) keyed by clusterid.
|
||
|
sub getClusterRateLimits {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
# (Re)build the rates table as necessary
|
||
|
unless ( %{$self->{ratelimits}} ) {
|
||
|
for my $clusterid ( keys %{$self->{jobs}} ) {
|
||
|
$self->{ratelimits}{ $clusterid } =
|
||
|
$self->getClusterRateLimit( $clusterid );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return wantarray ? %{$self->{ratelimits}} : $self->{ratelimits};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: setClusterRateLimit( $clusterid, $rate )
|
||
|
### Set the rate limit for the cluster with the given I<clusterid> to I<rate>.
|
||
|
sub setClusterRateLimit {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $clusterid, $rate ) = @_;
|
||
|
|
||
|
die "No clusterid" unless $clusterid;
|
||
|
die "No ratelimit" unless defined $rate && int($rate) == $rate;
|
||
|
|
||
|
# Set the new rule and trash the precalculated table
|
||
|
$self->{raterules}{ $clusterid } = $rate;
|
||
|
%{$self->{ratelimits}} = ();
|
||
|
|
||
|
return "Rate limit for cluster $clusterid set to $rate";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: setGlobalRateLimit( $rate )
|
||
|
### Set the rate limit for clusters that don't have an explicit ratelimit to
|
||
|
### I<rate>.
|
||
|
sub setGlobalRateLimit {
|
||
|
my JobServer $self = shift;
|
||
|
my $rate = shift;
|
||
|
die "No ratelimit" unless defined $rate && int($rate) == $rate;
|
||
|
|
||
|
# Set the global rule and clear out the cached table to rebuild it next time
|
||
|
# it's used
|
||
|
$self->{raterules}{global} = $rate;
|
||
|
%{$self->{ratelimits}} = ();
|
||
|
|
||
|
return "Global rate limit set to $rate";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: resetClusterRateLimit( $clusterid )
|
||
|
### Remove the explicit rate limit for the cluster with the given
|
||
|
### I<clusterid>. Returns the new limit for the cluster after resetting.
|
||
|
sub resetClusterRateLimit {
|
||
|
my JobServer $self = shift;
|
||
|
my $clusterid = shift or croak "No clusterid given.";
|
||
|
|
||
|
$self->debugMsg( 1, "Resetting rate limit for cluster $clusterid." );
|
||
|
delete $self->{raterules}{$clusterid};
|
||
|
%{$self->{ratelimits}} = ();
|
||
|
|
||
|
return $self->{raterules}{global} || $self->{config}{defaultRate};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: resetGlobalRateLimit( undef )
|
||
|
### Reset the rate limit for clusters that don't have an explicit ratelimit back
|
||
|
### to the default and returns it.
|
||
|
sub resetGlobalRateLimit {
|
||
|
my JobServer $self = shift;
|
||
|
delete $self->{raterules}{global};
|
||
|
%{$self->{ratelimits}} = ();
|
||
|
|
||
|
return $self->{config}{defaultRate};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: getJob( $client=JobServer::Client )
|
||
|
### Fetch a job for the given I<client> and return it. If there are no pending
|
||
|
### jobs, returns the undefined value.
|
||
|
sub getJob {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client ) = @_ or confess "No client object";
|
||
|
|
||
|
my (
|
||
|
$fd, # Client's fdno
|
||
|
$job, # Job arrayref
|
||
|
);
|
||
|
|
||
|
$fd = $client->fdno or confess "No file descriptor?!?";
|
||
|
$self->unassignJobForClient( $fd );
|
||
|
|
||
|
return $self->assignNextJob( $fd );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: assignNextJob( $fdno )
|
||
|
### Find the next pending job from the queue that would read from a non-busy
|
||
|
### source cluster, as determined by the rate limits given to the server. If one
|
||
|
### is found, assign it to the client associated with the given file descriptor
|
||
|
### I<fdno>. Returns the reply to be sent to the client.
|
||
|
sub assignNextJob {
|
||
|
my JobServer $self = shift;
|
||
|
my $fd = shift or return;
|
||
|
|
||
|
my (
|
||
|
$src, # Clusterid of a source
|
||
|
$rates, # Rate limits by clusterid
|
||
|
$jobcounts, # Counts of current jobs, by clusterid
|
||
|
@candidates, # Clusters with open slots
|
||
|
);
|
||
|
|
||
|
$rates = $self->getClusterRateLimits;
|
||
|
$jobcounts = $self->{jobcounts};
|
||
|
|
||
|
# Find clusterids of clusters with open slots, returning the undefined value
|
||
|
# if there are none.
|
||
|
@candidates = grep {
|
||
|
$jobcounts->{$_} < $rates->{$_}
|
||
|
} keys %{$self->{jobs}};
|
||
|
return undef unless @candidates;
|
||
|
|
||
|
# Pick a random cluster from the available list
|
||
|
$src = $candidates[ int rand(@candidates) ];
|
||
|
$self->debugMsg( 4, "Assigning job for cluster %d (%d of %d)",
|
||
|
$src, $jobcounts->{$src} + 1, $rates->{$src} );
|
||
|
|
||
|
# Assign the next job from that cluster and return it
|
||
|
return $self->assignJobFromCluster( $src, $fd );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: assignJobFromCluster( $clusterid, $fdno )
|
||
|
### Assign the next job from the cluster with the specified I<clusterid> to the
|
||
|
### client with the given file descriptor I<fdno>.
|
||
|
sub assignJobFromCluster {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $clusterid, $fdno ) = @_;
|
||
|
|
||
|
# Grab a job from the cluster's queue and add it to the assignments table.
|
||
|
my $job = $self->{assignments}{$fdno} = shift @{$self->{jobs}{$clusterid}};
|
||
|
$job->setFetchtime;
|
||
|
|
||
|
# Increment the job counter for that cluster and delete the queue if it's
|
||
|
# empty.
|
||
|
delete $self->{jobs}{$clusterid} if ! @{$self->{jobs}{$clusterid}};
|
||
|
$self->{jobcounts}{$clusterid}++;
|
||
|
|
||
|
# If there are more jobs for this queue, and the next job in the queue isn't
|
||
|
# prelocked, lock some more
|
||
|
$self->prelockSomeUsers
|
||
|
if exists $self->{jobs}{$clusterid}
|
||
|
&& ! $self->{jobs}{$clusterid}[0]->isPrelocked;
|
||
|
|
||
|
return $job;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: unassignJobForClient( $fdno )
|
||
|
### Unassign the job currently assigned to the client associated with the given
|
||
|
### I<fdno>.
|
||
|
sub unassignJobForClient {
|
||
|
my JobServer $self = shift;
|
||
|
my $fdno = shift or confess "No client fdno";
|
||
|
my $requeue = shift || '';
|
||
|
|
||
|
my (
|
||
|
$job,
|
||
|
$src,
|
||
|
);
|
||
|
|
||
|
# If there is a currently assigned job, we have work to do
|
||
|
if (( $job = delete $self->{assignments}{$fdno} )) {
|
||
|
$src = $job->srcclusterid;
|
||
|
|
||
|
# If the worker asked to finish it, assume it was completed and
|
||
|
# timedbuffer it for statistics.
|
||
|
if ( $job->isFinished ) {
|
||
|
$self->{recentmoves}->add( $job );
|
||
|
}
|
||
|
|
||
|
# Otherwise, requeue it if that's enabled
|
||
|
else {
|
||
|
|
||
|
if ( $requeue ) {
|
||
|
$self->logMsg( 'info', "Re-adding job %s to queue", $job->stringify );
|
||
|
$self->{jobs}{ $job->srcclusterid } ||= [];
|
||
|
unshift @{$self->{jobs}{ $job->srcclusterid }}, $job;
|
||
|
}
|
||
|
|
||
|
# Free up a slot on the source
|
||
|
$self->debugMsg( 3, "Client %d dropped job %s", $fdno, $job->stringify );
|
||
|
}
|
||
|
|
||
|
# Delete the user's job and decrement the job count for the cluster the
|
||
|
# job belonged to
|
||
|
delete $self->{users}{ $job->userid };
|
||
|
$self->{jobcounts}{ $src }--;
|
||
|
$self->debugMsg( 3, "Cluster %d now has %d clients",
|
||
|
$src, $self->{jobcounts}{ $src } );
|
||
|
}
|
||
|
|
||
|
return $job;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: getJobForUser( $userid )
|
||
|
### Return the job associated with a given userid.
|
||
|
sub getJobForUser {
|
||
|
my JobServer $self = shift;
|
||
|
my $userid = shift or confess "No userid specified";
|
||
|
|
||
|
return $self->{users}{ $userid };
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: stopAllJobs( $client=JobServer::Client )
|
||
|
### Stop all pending and currently-assigned jobs.
|
||
|
sub stopAllJobs {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or confess "No client object";
|
||
|
|
||
|
$self->stopNewJobs( $client );
|
||
|
$self->logMsg( 'notice', "Clearing currently-assigned jobs." );
|
||
|
%{$self->{assignments}} = ();
|
||
|
%{$self->{jobs}} = ();
|
||
|
%{$self->{jobcounts}} = ();
|
||
|
%{$self->{users}} = ();
|
||
|
|
||
|
return "Cleared all jobs.";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: stopNewJobs( $client=JobServer::Client )
|
||
|
### Stop assigning pending jobs.
|
||
|
sub stopNewJobs {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or confess "No client object";
|
||
|
|
||
|
$self->logMsg( 'notice', "Clearing pending jobs." );
|
||
|
%{$self->{jobs}} = ();
|
||
|
foreach my $userid ( keys %{$self->{users}} ) {
|
||
|
delete $self->{users}{ $userid } unless $self->{users}{$userid}->isFetched;
|
||
|
}
|
||
|
|
||
|
return "Cleared pending jobs.";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: requestJobFinish( $client=JobServer::Client, $userid, $srcclusterid, $dstclusterid )
|
||
|
### Request authorization to finish a given job.
|
||
|
sub requestJobFinish {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $userid, $srcclusterid, $dstclusterid ) = @_;
|
||
|
|
||
|
my (
|
||
|
$fdno, # The client's fdno
|
||
|
$job, # The client's currently assigned job
|
||
|
);
|
||
|
|
||
|
# Fetch the fdno of the client and try to get the job object they were last
|
||
|
# assigned. If it doesn't exist, all jobs are stopped or something else has
|
||
|
# happened, so advise the client to abort.
|
||
|
$fdno = $client->fdno;
|
||
|
if ( ! exists $self->{assignments}{$fdno} ) {
|
||
|
$self->logMsg( 'warn', "Client $fdno: finish on unassigned job" );
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
# If the job the client was last assigned doesn't match the userid they've
|
||
|
# specified, abort.
|
||
|
$job = $self->{assignments}{$fdno};
|
||
|
if ( $job->userid != $userid ) {
|
||
|
$self->logMsg( 'warn', "Client %d: finish for non-assigned job %s",
|
||
|
$fdno, $job->stringify );
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
# Otherwise mark the job as finished and advise the client that they can
|
||
|
# proceed.
|
||
|
$job->setFinishTime;
|
||
|
$self->debugMsg( 2, 'Client %d finishing job %s',
|
||
|
$fdno, $job->stringify );
|
||
|
|
||
|
return "Go ahead with job " . $job->stringify;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### METHOD: getJobList( undef )
|
||
|
### Return a hashref of job stats. The hashref will contain three arrays: the
|
||
|
### 'queued_jobs' array contains a line describing how many jobs are queued for
|
||
|
### each source cluster, the 'assigned_jobs' array contains a line per client
|
||
|
### that's currently moving a user, and the 'footer' array contains some lines
|
||
|
### of overall statistics about the server.
|
||
|
sub getJobList {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
my (
|
||
|
%stats, # The returned job stats
|
||
|
$queuedCount, # Number of queued jobs
|
||
|
$assignedCount, # Number of jobs currently assigned
|
||
|
$job, # Job object iterator
|
||
|
$rates, # Rate-limit table
|
||
|
);
|
||
|
|
||
|
%stats = ( queued_jobs => [], assigned_jobs => [], footer => [] );
|
||
|
$queuedCount = $assignedCount = 0;
|
||
|
$rates = $self->getClusterRateLimits;
|
||
|
|
||
|
# The first sublist: queued jobs
|
||
|
foreach my $clusterid ( sort keys %{$self->{jobs}} ) {
|
||
|
push @{$stats{queued_jobs}},
|
||
|
sprintf( "%3d: %5d jobs queued @ limit %d",
|
||
|
$clusterid,
|
||
|
scalar @{$self->{jobs}{$clusterid}},
|
||
|
$rates->{$clusterid} );
|
||
|
$queuedCount += scalar @{$self->{jobs}{$clusterid}};
|
||
|
}
|
||
|
|
||
|
# Second sublist: assigned jobs
|
||
|
foreach my $fdno ( sort keys %{$self->{assignments}} ) {
|
||
|
$job = $self->{assignments}{$fdno};
|
||
|
push @{$stats{assigned_jobs}},
|
||
|
sprintf( "%3d: working on moving %7d from %3d to %3d",
|
||
|
$fdno, $job->userid, $job->srcclusterid,
|
||
|
$job->dstclusterid );
|
||
|
$assignedCount++;
|
||
|
}
|
||
|
|
||
|
# Append the footer lines
|
||
|
push @{$stats{footer}},
|
||
|
sprintf( " %d queued jobs, %d assigned jobs for %d clusters",
|
||
|
$queuedCount, $assignedCount, scalar keys %{$self->{jobs}} );
|
||
|
|
||
|
if ( $self->{totaljobs} ) {
|
||
|
push @{$stats{footer}},
|
||
|
sprintf( " %d of %d total jobs assigned since %s (%0.1f/s)",
|
||
|
$self->{totaljobs} - $queuedCount,
|
||
|
$self->{totaljobs},
|
||
|
scalar localtime($self->{starttime}),
|
||
|
(time - $self->{starttime}) / ($self->{totaljobs})
|
||
|
);
|
||
|
} else {
|
||
|
push @{$stats{footer}},
|
||
|
sprintf( " No jobs assigned since startup (%s)",
|
||
|
scalar localtime($self->{starttime}) );
|
||
|
}
|
||
|
|
||
|
return \%stats;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: getSourceCount( undef )
|
||
|
### Return a hash (or hashref in scalar context) of srcclusterids => # of
|
||
|
### pending (queued) jobs.
|
||
|
sub getJobCounts {
|
||
|
my JobServer $self = shift;
|
||
|
my %rhash = map { $_ => scalar @{$self->{jobs}{$_}} } keys %{$self->{jobs}};
|
||
|
|
||
|
return wantarray ? %rhash : \%rhash;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: shutdown( $agent )
|
||
|
### Shut the server down.
|
||
|
sub shutdown {
|
||
|
my JobServer $self = shift;
|
||
|
my $agent = shift;
|
||
|
|
||
|
# Stop incoming connections (:TODO: remove it from Danga::Socket?)
|
||
|
$self->{listener}->close;
|
||
|
|
||
|
# Clear jobs so no more get handed out while clients are closing
|
||
|
$self->{jobs} = {};
|
||
|
$self->{users} = {};
|
||
|
$self->logMsg( 'notice', "Server shutdown by %s", $agent->stringify );
|
||
|
|
||
|
# Drop all clients
|
||
|
foreach my $client ( values %{$self->{clients}} ) {
|
||
|
$client->write( "Server shutdown.\r\n" );
|
||
|
$client->close;
|
||
|
}
|
||
|
|
||
|
exit;
|
||
|
}
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### E V E N T S U B S Y S T E M M E T H O D S
|
||
|
#####################################################################
|
||
|
|
||
|
### METHOD: handleEvent( $type, @args )
|
||
|
### Handle an event of the given I<type> with the specified I<args>.
|
||
|
sub handleEvent {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $type, @args ) = @_;
|
||
|
|
||
|
# Invoke each registered handler for the given type
|
||
|
for my $func ( values %{$self->{handlers}{$type}} ) {
|
||
|
$func->( @args );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: handlers( [$type] )
|
||
|
### Return a hash of all registered handlers of the given I<type>, or all
|
||
|
### handlers keyed by type if no type is specified.
|
||
|
sub handlers {
|
||
|
my JobServer $self = shift;
|
||
|
my $type = shift || '';
|
||
|
|
||
|
my $rhash;
|
||
|
|
||
|
if ( $type ) {
|
||
|
$rhash = $self->{handlers}{$type};
|
||
|
} else {
|
||
|
$rhash = $self->{handlers};
|
||
|
}
|
||
|
|
||
|
return () unless $rhash;
|
||
|
return wantarray ? %$rhash : $rhash;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: addHandlerToAll( $key, \&code )
|
||
|
### Add the specified callback (I<code>) as an event handler for all implemented
|
||
|
### event types. The associated I<key> can be used to later remove the
|
||
|
### handler/s. Returns the number of event types subscribed to.
|
||
|
sub addHandlerToAll {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $key, $code ) = @_;
|
||
|
|
||
|
my $count = 0;
|
||
|
foreach my $type ( keys %{$self->{handlers}} ) {
|
||
|
$count++ if $self->addHandler( $type, $key, $code );
|
||
|
}
|
||
|
|
||
|
return $count;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: removeHandlerFromAll( $key )
|
||
|
### Remove all event callbacks for the specified I<key>. Returns the number of
|
||
|
### handlers removed.
|
||
|
sub removeHandlerFromAll {
|
||
|
my JobServer $self = shift;
|
||
|
my $key = shift;
|
||
|
|
||
|
my $count = 0;
|
||
|
foreach my $type ( keys %{$self->{handlers}} ) {
|
||
|
$count++ if $self->removeHandler( $type, $key );
|
||
|
}
|
||
|
|
||
|
return $count;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: addHandler( $type, $key, \&code )
|
||
|
### Add a callback (I<code>) that handles events of the given I<type>. The
|
||
|
### I<key> argument can be used to later remove the handler.
|
||
|
sub addHandler {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $type, $key, $code ) = @_;
|
||
|
|
||
|
confess "No such event type '$type'"
|
||
|
unless exists $self->{handlers}{$type};
|
||
|
confess "$type handler for '$key' is a ",
|
||
|
(ref $code ? "simple scalar '$code'" : ref $code),
|
||
|
", not a CODE ref." unless ref $code eq 'CODE';
|
||
|
|
||
|
$self->{handlers}{$type}{ $key } = $code;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: removeHandler( $type, $key )
|
||
|
### Remove and return the callback associated with the specified I<key> and
|
||
|
### event I<type>.
|
||
|
sub removeHandler {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $type, $key ) = @_;
|
||
|
|
||
|
no warnings 'uninitialized';
|
||
|
return delete $self->{handlers}{$type}{ $key };
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: subscribe( $client=JobServer::Client, $type, $args )
|
||
|
### Subscribe the given I<client> to the given I<type> of server events with the
|
||
|
### given I<args>.
|
||
|
sub subscribe {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $type, $args ) = @_;
|
||
|
|
||
|
my $method = sprintf( 'subscribe%sEvents', ucfirst $type );
|
||
|
my $func = $self->can( $method )
|
||
|
or die "No such event type '$type' (No $method method)";
|
||
|
$self->debugMsg( 2, "Subscribing client %d to %s events via %s(%s)",
|
||
|
$client->fdno, $type, $method, $args );
|
||
|
|
||
|
return $func->( $self, $client, $args );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: unsubscribe( $client=JobServer::Client, $type, $args )
|
||
|
### Unsubscribe the given I<client> to the given I<type> of server events with the
|
||
|
### given I<args>.
|
||
|
sub unsubscribe {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $type ) = @_;
|
||
|
|
||
|
my $method = sprintf( 'unsubscribe%sEvents', ucfirst $type );
|
||
|
my $func = $self->can( $method )
|
||
|
or die "No such event type '$type' (No $method method)";
|
||
|
|
||
|
return $func->( $self, $client );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: subscribeLogEvents( $client, $level )
|
||
|
### Register a log event handler for the specified I<client> at the given
|
||
|
### I<level>, replacing any currently-extant one.
|
||
|
sub subscribeLogEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $level ) = @_;
|
||
|
my $ll = $LogLevels{ $level };
|
||
|
|
||
|
my $callback = sub {
|
||
|
my ( $loglevel, $msg ) = @_;
|
||
|
return () unless $LogLevels{$loglevel} >= $ll;
|
||
|
$client->eventMessage( 'log', "[$loglevel] $msg" );
|
||
|
};
|
||
|
|
||
|
$self->addHandler( 'log', $client->fdno, $callback );
|
||
|
|
||
|
return "Subscribed to log events for level '$level'";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: unsubscribeLogEvents( $client )
|
||
|
### Unregister the log handler registered for the given I<client>.
|
||
|
sub unsubscribeLogEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or croak "No client";
|
||
|
|
||
|
$self->removeHandler( 'log', $client->fdno );
|
||
|
return "Unsubscribed from log events.";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: subscribeDebugEvents( $client, $level )
|
||
|
### Register a debug event handler for the specified I<client> at the given
|
||
|
### I<level>, replacing any currently-extant one.
|
||
|
sub subscribeDebugEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my ( $client, $level ) = @_;
|
||
|
|
||
|
my $callback = sub {
|
||
|
my ( $debuglevel, $msg ) = @_;
|
||
|
return () unless $debuglevel <= $level;
|
||
|
$client->eventMessage( 'debug', "[$debuglevel] $msg" );
|
||
|
};
|
||
|
|
||
|
$self->addHandler( 'debug', $client->fdno, $callback );
|
||
|
|
||
|
return "Subscribed to debug events for level '$level'";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: unsubscribeDebugEvents( $client )
|
||
|
### Unregister the debug handler registered for the given I<client>.
|
||
|
sub unsubscribeDebugEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or croak "No client";
|
||
|
|
||
|
$self->removeHandler( 'debug', $client->fdno );
|
||
|
return "Unsubscribed from debug events.";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: subscribeAddEvents( $client, $level )
|
||
|
### Register an 'add' event handler for the specified I<client>.
|
||
|
sub subscribeAddEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or croak "No client";
|
||
|
|
||
|
my $callback = sub {
|
||
|
my $count = shift;
|
||
|
$client->eventMessage( 'add', "$count jobs added" );
|
||
|
};
|
||
|
|
||
|
$self->addHandler( 'add', $client->fdno, $callback );
|
||
|
|
||
|
return "Subscribed to add events";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: unsubscribeAddEvents( $client )
|
||
|
### Unregister the 'add' handler registered for the given I<client>.
|
||
|
sub unsubscribeAddEvents {
|
||
|
my JobServer $self = shift;
|
||
|
my $client = shift or croak "No client";
|
||
|
|
||
|
$self->removeHandler( 'add', $client->fdno );
|
||
|
return "Unsubscribed from add events.";
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### ' P R O T E C T E D ' M E T H O D S
|
||
|
#####################################################################
|
||
|
|
||
|
|
||
|
### METHOD: daemonize( undef )
|
||
|
### Double fork and become a good little daemon
|
||
|
sub daemonize {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
$self->stubbornFork( 5 ) && exit 0;
|
||
|
|
||
|
# Become session leader to detach from controlling tty
|
||
|
POSIX::setsid() or croak "Couldn't become session leader: $!";
|
||
|
|
||
|
# Fork again, ignore hangup to avoid reacquiring a controlling tty
|
||
|
{
|
||
|
local $SIG{HUP} = 'IGNORE';
|
||
|
$self->stubbornFork( 5 ) && exit 0;
|
||
|
}
|
||
|
|
||
|
# Change working dir to the filesystem root, clear the umask
|
||
|
chdir "/";
|
||
|
umask 0;
|
||
|
|
||
|
# Close standard file descriptors and reopen them to /dev/null
|
||
|
close STDIN && open STDIN, "/dev/null";
|
||
|
close STDOUT && open STDOUT, ">/dev/null";
|
||
|
close STDERR && open STDERR, "+>&STDOUT";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: stubbornFork( $maxTries )
|
||
|
### Attempt to fork through errors
|
||
|
sub stubbornFork {
|
||
|
my JobServer $self = shift;
|
||
|
my $maxTries = shift || 5;
|
||
|
|
||
|
my(
|
||
|
$pid,
|
||
|
$tries,
|
||
|
);
|
||
|
|
||
|
$tries = 0;
|
||
|
FORK: while ( $tries <= $maxTries ) {
|
||
|
if (( $pid = fork )) {
|
||
|
return $pid;
|
||
|
} elsif ( defined $pid ) {
|
||
|
return 0;
|
||
|
} elsif ( $! =~ m{no more process} ) {
|
||
|
sleep 5;
|
||
|
next FORK;
|
||
|
} else {
|
||
|
die "Cannot fork: $!";
|
||
|
}
|
||
|
} continue {
|
||
|
$tries++;
|
||
|
}
|
||
|
|
||
|
die "Failed to fork after $tries tries: $!";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: debugLevel( [$newLevel] )
|
||
|
### Get/set the server's debugging level.
|
||
|
sub debugLevel {
|
||
|
my JobServer $self = shift;
|
||
|
|
||
|
$self->{config}{debugLevel} = (shift || 0) if @_;
|
||
|
return $self->{config}{debugLevel};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: debugMsg( $level, $format, @args )
|
||
|
### If the debug level is C<$level> or above, and there are debug handlers
|
||
|
### defined, call each of them at the specified level with the given printf
|
||
|
### C<$format> and C<@args>.
|
||
|
sub debugMsg {
|
||
|
my JobServer $self = shift;
|
||
|
my $level = shift;
|
||
|
my $debugLevel = $self->{config}{debugLevel};
|
||
|
return unless $level && $debugLevel >= abs $level;
|
||
|
return unless %{$self->{handlers}{debug}};
|
||
|
|
||
|
my $msg = shift;
|
||
|
$msg =~ s{[\r\n]+$}{};
|
||
|
|
||
|
if ( $debugLevel > 1 ) {
|
||
|
my $caller = caller;
|
||
|
$msg = "<$caller> $msg";
|
||
|
}
|
||
|
|
||
|
# Call each subscribed debug event handler with the level and message.
|
||
|
$msg = $self->formatLogMsg( $msg, @_ );
|
||
|
$self->handleEvent( 'debug', $level, $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: logMsg( $level, $format, @args )
|
||
|
### Call any log handlers that have been defined at the specified level with the
|
||
|
### given printf C<$format> and C<@args>.
|
||
|
sub logMsg {
|
||
|
my JobServer $self = shift;
|
||
|
return () unless %{$self->{handlers}{log}};
|
||
|
my $level = shift or return ();
|
||
|
my $msg = $self->formatLogMsg( @_ );
|
||
|
|
||
|
$self->handleEvent( 'log', $level, $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: formatLogMsg( $format, @args )
|
||
|
### Create and return a message for the given C<sprintf()>-style I<format> and
|
||
|
### I<args>, dumping any complex datatypes and marking the undefined value.
|
||
|
sub formatLogMsg {
|
||
|
my JobServer $self = shift;
|
||
|
my $format = shift;
|
||
|
|
||
|
# Fetch level and format and strip returns off the latter.
|
||
|
$format =~ s{[\r\n]+$}{};
|
||
|
|
||
|
# Turn any references or undefined values in the arglist into dumped strings
|
||
|
my @args = map {
|
||
|
defined $_ ?
|
||
|
(ref $_ ? Data::Dumper->Dumpxs([$_], [ref $_]) : $_) :
|
||
|
'(undef)'
|
||
|
} @_;
|
||
|
return sprintf( $format, @args );
|
||
|
}
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### J O B C L A S S
|
||
|
#####################################################################
|
||
|
package JobServer::Job;
|
||
|
use strict;
|
||
|
|
||
|
BEGIN {
|
||
|
use Carp qw{croak confess};
|
||
|
use Time::HiRes qw{time};
|
||
|
use Scalar::Util qw{blessed};
|
||
|
|
||
|
use lib "$ENV{LJHOME}/cgi-bin";
|
||
|
require 'ljlib.pl';
|
||
|
require 'ljconfig.pl';
|
||
|
|
||
|
use fields (
|
||
|
'server', # The server this job belongs to
|
||
|
'userid', # The userid of the user to move
|
||
|
'srcclusterid', # The cluster id of the source cluster
|
||
|
'dstclusterid', # Cluster id of the destination cluster
|
||
|
'createtime', # Epoch time of job creation
|
||
|
'prelocktime', # Epoch time of prelock, 0 if not prelocked
|
||
|
'fetchtime', # Time the job was given to a mover, 0 if unassigned
|
||
|
'finishtime', # Epoch time of server finish authorization
|
||
|
'options', # Job options passed between populator and mover
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
### Class globals
|
||
|
our ( $ReadOnlyCapBit );
|
||
|
|
||
|
INIT {
|
||
|
# Find the readonly cap class, complain if not found
|
||
|
$ReadOnlyCapBit = undef;
|
||
|
|
||
|
# Find the moveinprogress bit from the caps hash
|
||
|
foreach my $bit ( keys %LJ::CAP ) {
|
||
|
next unless exists $LJ::CAP{$bit}{_name};
|
||
|
if ( $LJ::CAP{$bit}{_name} eq '_moveinprogress' &&
|
||
|
$LJ::CAP{$bit}{readonly} == 1 )
|
||
|
{
|
||
|
$ReadOnlyCapBit = $bit;
|
||
|
last;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
die "Cannot mark user readonly without a ReadOnlyCapBit. Check %LJ::CAP"
|
||
|
unless $ReadOnlyCapBit;
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
### (CONSTRUCTOR) METHOD: new( [$userid, $srcclusterid, $dstclusterid )
|
||
|
### Create and return a new JobServer::Job object.
|
||
|
sub new {
|
||
|
my JobServer::Job $self = shift;
|
||
|
my $server = shift or confess "no server object";
|
||
|
croak "Illegal first argument: expected a JobServer::Client, got a ",
|
||
|
ref $server ? ref $server : "simple scalar ('$server')"
|
||
|
unless blessed $server && $server->isa( 'JobServer::Client' );
|
||
|
|
||
|
$self = fields::new( $self ) unless ref $self;
|
||
|
|
||
|
# Split instance vars from a string with a colon in the second or later
|
||
|
# position
|
||
|
if ( index($_[0], ':') > 0 ) {
|
||
|
my ( $idtuple, $options ) = split /\s+/, $_[0], 2;
|
||
|
|
||
|
# Split '<uid>:<scid>:<dcid>' into members
|
||
|
@{$self}{qw{userid srcclusterid dstclusterid}} = split /:/, $idtuple, 3;
|
||
|
|
||
|
# Split '<k>=<v> <k2>=<v2>' into a hashref
|
||
|
if ( $options ) {
|
||
|
$self->{options} = {
|
||
|
map { split /=/, $_, 2 } split(/\s+/, $options)
|
||
|
};
|
||
|
} else {
|
||
|
$self->{options} = {};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Allow list arguments as well
|
||
|
else {
|
||
|
# First 3 args are id members
|
||
|
@{$self}{qw{userid srcclusterid dstclusterid}} =
|
||
|
splice( @_, 0, 3 );
|
||
|
|
||
|
# Any remaining are assumed to be pairs in an options hash
|
||
|
$self->{options} = { @_ };
|
||
|
}
|
||
|
|
||
|
# Check for the stuff we need
|
||
|
croak "Invalid job specifications: No userid"
|
||
|
unless defined $self->{userid};
|
||
|
croak "Invalid job specifications: No source clusterid"
|
||
|
unless defined $self->{srcclusterid};
|
||
|
croak "Invalid job specifications: No destination clusterid"
|
||
|
unless defined $self->{dstclusterid};
|
||
|
|
||
|
$self->{server} = $server;
|
||
|
$self->{createtime} = time;
|
||
|
$self->{prelocktime} = 0.0;
|
||
|
$self->{fetchtime} = 0.0;
|
||
|
$self->{finishtime} = 0.0;
|
||
|
|
||
|
return $self;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: userid( [$newuserid] )
|
||
|
### Get/set the job's userid.
|
||
|
sub userid {
|
||
|
my JobServer::Job $self = shift;
|
||
|
$self->{userid} = shift if @_;
|
||
|
return $self->{userid};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: srcclusterid( [$newsrcclusterid] )
|
||
|
### Get/set the job's srcclusterid.
|
||
|
sub srcclusterid {
|
||
|
my JobServer::Job $self = shift;
|
||
|
$self->{srcclusterid} = shift if @_;
|
||
|
return $self->{srcclusterid};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: dstclusterid( [$newdstclusterid] )
|
||
|
### Get/set the job's dstclusterid.
|
||
|
sub dstclusterid {
|
||
|
my JobServer::Job $self = shift;
|
||
|
$self->{dstclusterid} = shift if @_;
|
||
|
return $self->{dstclusterid};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: stringify( undef )
|
||
|
### Return a scalar containing the stringified representation of the job.
|
||
|
sub stringify {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return sprintf( '%d:%d:%d %0.1f %s',
|
||
|
@{$self}{'userid', 'srcclusterid', 'dstclusterid'},
|
||
|
$self->secondsSinceLock, $self->optString );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: prettyString( undef )
|
||
|
### Return a less-parseable, but more-readable string representation of the job
|
||
|
### than C<stringify()> provides.
|
||
|
sub prettyString {
|
||
|
my JobServer::Job $self = shift;
|
||
|
|
||
|
return sprintf( "User %d %s from %d to %d (%s):\n\t%s",
|
||
|
$self->{userid},
|
||
|
$self->verb,
|
||
|
$self->{srcclusterid},
|
||
|
$self->{dstclusterid},
|
||
|
$self->optString,
|
||
|
$self->timeString,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: verb( undef )
|
||
|
### Return the correct conjugation of the verb "to move" that would describe the
|
||
|
### job given its current state.
|
||
|
sub verb {
|
||
|
my JobServer::Job $self = shift;
|
||
|
|
||
|
return "moved" if $self->{finishtime};
|
||
|
return "moving" if $self->{fetchtime};
|
||
|
return "to move";
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: optString( undef )
|
||
|
### Return the job's options as a string.
|
||
|
sub optString {
|
||
|
my JobServer::Job $self = shift;
|
||
|
my $opts = $self->{options};
|
||
|
return join( " ", map { "$_=$opts->{$_}" } keys %$opts );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: timeString( undef )
|
||
|
### Return the job's various timestamps (if set).
|
||
|
sub timeString {
|
||
|
my JobServer::Job $self = shift;
|
||
|
my @parts = ();
|
||
|
|
||
|
push @parts, sprintf( '%0.2fs old', $self->age );
|
||
|
push @parts, sprintf( 'locked %0.2fs', $self->secondsSinceLock )
|
||
|
if $self->{prelocktime};
|
||
|
push @parts,
|
||
|
sprintf( 'fetched %0.2fs ago (%0.1fs queued)',
|
||
|
$self->secondsSinceFetch,
|
||
|
$self->{fetchtime} - $self->{createtime} )
|
||
|
if $self->{fetchtime};
|
||
|
push @parts,
|
||
|
sprintf( 'finished in %0.2fs', $self->aliveTime )
|
||
|
if $self->{finishtime};
|
||
|
|
||
|
return join ", ", @parts;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: prelock( undef )
|
||
|
### Mark the user in this job read-only and set the prelocktime.
|
||
|
sub prelock {
|
||
|
my JobServer::Job $self = shift;
|
||
|
|
||
|
my $dbh = LJ::get_db_writer()
|
||
|
or return 0;
|
||
|
|
||
|
# both before and after updating a user's read-only flag we add the
|
||
|
# user to the 'readonly_user' table, which is just an index onto
|
||
|
# users who /might/ be in read-only. another cronjob can periodically
|
||
|
# clean those and make sure nobody is stranded in readonly, without
|
||
|
# resorting to a full tablescan of the user table.
|
||
|
$dbh->do("INSERT IGNORE INTO readonly_user SET userid=?", undef, $self->{userid});
|
||
|
my $rval = LJ::update_user( $self->{userid},
|
||
|
{raw => "caps = caps | (1<<$ReadOnlyCapBit)"} );
|
||
|
|
||
|
if ( $rval ) {
|
||
|
$dbh->do("INSERT IGNORE INTO readonly_user SET userid=?", undef, $self->{userid});
|
||
|
$self->setPrelocktime;
|
||
|
$self->{options}{prelocked} = 1;
|
||
|
$self->{server}->debugMsg( 4, q{Prelocked user %d}, $self->{userid} );
|
||
|
} else {
|
||
|
$self->{server}->logMsg( 'warn', q{Couldn't prelock user %d: %s},
|
||
|
$self->{userid}, $DBI::errstr );
|
||
|
}
|
||
|
|
||
|
return $self->{prelocktime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: prelocktime( [$newprelocktime] )
|
||
|
### Get the floating-point epoch time when the user record corresponding to the
|
||
|
### job's C<userid> was set read-only.
|
||
|
sub prelocktime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{prelocktime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: setPrelocktime( undef )
|
||
|
### Set the prelocktime to the current floating-point epoch time.
|
||
|
sub setPrelocktime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{prelocktime} = time;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: secondsSinceLock( undef )
|
||
|
### Return the number of seconds since the job's user was prelocked, or 0 if the
|
||
|
### user isn't prelocked.
|
||
|
sub secondsSinceLock {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return 0.0 unless $self->{prelocktime};
|
||
|
return time - $self->{prelocktime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: isPrelocked( undef )
|
||
|
### Returns a true value if the user corresponding to the job has already been
|
||
|
### marked read-only.
|
||
|
sub isPrelocked {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{prelocktime} != 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: finishTime( [$newtime] )
|
||
|
### Returns the floating-point epoch time when the job was 'finished'.
|
||
|
sub finishTime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{finishtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: setFinishTime( undef )
|
||
|
### Set the finishtime to the current floating-point epoch time.
|
||
|
sub setFinishTime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{finishtime} = time;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: secondsSinceFinish( undef )
|
||
|
### Returns the number of seconds that have elapsed since the job was
|
||
|
### 'finished'.
|
||
|
sub secondsSinceFinish {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return 0 unless $self->{finishtime};
|
||
|
return time - $self->{finishtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: isFinished( undef )
|
||
|
### Returns a true value if the mover has requested authorization from the
|
||
|
### jobserver to finish the job.
|
||
|
sub isFinished {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{finishtime} != 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: fetchtime( undef )
|
||
|
### Get the floatin-point epoch time when the job was fetched by a mover.
|
||
|
sub fetchtime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{fetchtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: setFetchtime( undef )
|
||
|
### Set the fetchtime to the current floating-point epoch time.
|
||
|
sub setFetchtime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{fetchtime} = time;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: secondsSinceFetch( undef )
|
||
|
### Return the number of seconds since the job was fetched by a mover.
|
||
|
sub secondsSinceFetch {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return 0 unless $self->{fetchtime};
|
||
|
return time - $self->{fetchtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: isFetched( undef )
|
||
|
### Returns a true value if the job has been assigned to a mover.
|
||
|
sub isFetched {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{fetchtime} != 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: createTime( undef )
|
||
|
### Return the floating-point epoch time when the job was created.
|
||
|
sub createTime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return $self->{createtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: age( undef )
|
||
|
### Return the number of floating-point seconds since the job was created.
|
||
|
sub age {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return time - $self->{createtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: aliveTime( undef )
|
||
|
### Return the number of floating-point seconds the job was alive, which is the
|
||
|
### time between when it was created and when it was finished.
|
||
|
sub aliveTime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return 0 unless $self->{finishtime};
|
||
|
return $self->{finishtime} - $self->{createtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: activeTime( undef )
|
||
|
### Return the number of floating-point seconds the job was active, which is the
|
||
|
### time between when it was fetched and when it was finished.
|
||
|
sub activeTime {
|
||
|
my JobServer::Job $self = shift;
|
||
|
return 0 unless $self->{finishtime} && $self->{fetchtime};
|
||
|
return $self->{finishtime} - $self->{fetchtime};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: debugMsg( $level, $format, @args )
|
||
|
### Send a debugging message to the server this job belongs to.
|
||
|
sub debugMsg {
|
||
|
my JobServer::Job $self = shift;
|
||
|
$self->{server}->debugMsg( @_ );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: logMsg( $type, $format, @args )
|
||
|
### Send a log message to the server this job belongs to.
|
||
|
sub logMsg {
|
||
|
my JobServer::Job $self = shift;
|
||
|
$self->{server}->logMsg( @_ );
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### C L I E N T B A S E C L A S S
|
||
|
#####################################################################
|
||
|
package JobServer::Client;
|
||
|
|
||
|
# Props to Junior for lots of this code, stolen largely from the SPUD server.
|
||
|
|
||
|
BEGIN {
|
||
|
use Carp qw{croak confess};
|
||
|
use base qw{Danga::Socket};
|
||
|
use fields qw{read_buf server state};
|
||
|
}
|
||
|
|
||
|
|
||
|
our ( $Tuple, $JobOption, $JobSpec, %CommandTable, $CommandPattern );
|
||
|
|
||
|
INIT {
|
||
|
|
||
|
# Pattern for matching job id tuples of the form:
|
||
|
# <userid>:<srcclusterid>:<dstclusterid>
|
||
|
$Tuple = qr{\d+:\d+:\d+};
|
||
|
|
||
|
# Pattern for matching one job-spec option which is a moveucluster option
|
||
|
# key-value pair in the form:
|
||
|
# <optioname>=<optval>
|
||
|
$JobOption = qr{\s+\w+=\w+};
|
||
|
|
||
|
# Pattern for matching a whole job-spec
|
||
|
$JobSpec = qr{$Tuple$JobOption*};
|
||
|
|
||
|
# Commands the server understands. Each entry should be paired with a method
|
||
|
# called cmd_<command_name>. The 'args' element contains a regexp for
|
||
|
# matching the command's arguments after whitespace-stripping on both sides;
|
||
|
# any capture-groups will be passed to the method as arguments. Commands
|
||
|
# which don't match the argument pattern will produce an error
|
||
|
# message. E.g., if the pattern for 'foo_bar' is /^(\w+)\s+(\d+)$/, then
|
||
|
# entering the command "foo_bar frobnitz 4" would call:
|
||
|
# ->cmd_foo_bar( "frobnitz", "4" )
|
||
|
#
|
||
|
# The 'help' element of each command is used to provide the information
|
||
|
# necessary for the 'help' command.
|
||
|
#
|
||
|
# If an entry contains a 'form' element, it will be used to describe the
|
||
|
# arguments which are expected/required by the command, and is used in the
|
||
|
# 'form' part of the individual help for that command. If it is omitted, the
|
||
|
# command is assumed by the help system to be standalone and take no arguments.
|
||
|
%CommandTable = (
|
||
|
|
||
|
# :TODO: Implement a 'desc' or 'longhelp' or something to augment the
|
||
|
# per-command help.
|
||
|
|
||
|
get_job => {
|
||
|
help => "get a job (from mover)",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
add_jobs => {
|
||
|
help => "add one or more new jobs",
|
||
|
form => "<userid>:<srcclusterid>:<dstclusterid> <options>[, ...]",
|
||
|
args => qr{^((?:$JobSpec\s*,\s*)*$JobSpec)$},
|
||
|
},
|
||
|
|
||
|
source_counts => {
|
||
|
help => "dump pending jobs per source cluster",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
stop_moves => {
|
||
|
help => "stop all moves",
|
||
|
form => "[all]",
|
||
|
args => qr{^(all)?$},
|
||
|
},
|
||
|
|
||
|
is_moving => {
|
||
|
help => "check to see if a user is being moved",
|
||
|
form => "<userid>",
|
||
|
args => qr{^(\d+)$},
|
||
|
},
|
||
|
|
||
|
list_jobs => {
|
||
|
help => "list internal state",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
move_stats => {
|
||
|
help => "List recent move statistics",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
recent_moves => {
|
||
|
help => "Show a log of recent moves",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
set_rate => {
|
||
|
help => "Set the rate for a given source cluster or for all clusters",
|
||
|
form => "<globalrate> or <srcclusterid>:<rate>",
|
||
|
args => qr{^(\d+)(?:[:\s]+(\d+))?\s*$},
|
||
|
},
|
||
|
|
||
|
show_rates => {
|
||
|
help => "Show the rate settings for all clusters",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
reset_rate => {
|
||
|
help => "Clear rate settings for all or the given source cluster/s.",
|
||
|
form => "[<srcclusterid>]",
|
||
|
args => qr{^(\d+)?$},
|
||
|
},
|
||
|
|
||
|
finish => {
|
||
|
help => "request authorization to complete a move job",
|
||
|
form => "<userid>:<srcclusterid>:<dstclusterid>",
|
||
|
args => qr{^($Tuple)$},
|
||
|
},
|
||
|
|
||
|
quit => {
|
||
|
help => "disconnect from the server",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
shutdown => {
|
||
|
help => "shut the server down",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
lock => {
|
||
|
help => "Pre-lock a given user's job. The job must have already been added.",
|
||
|
form => '<userid>',
|
||
|
args => qr{^(\d+)$},
|
||
|
},
|
||
|
|
||
|
clients => {
|
||
|
help => "Show a list of connected clients",
|
||
|
args => qr{^$},
|
||
|
},
|
||
|
|
||
|
subscribe => {
|
||
|
help => "Subscribe to server events",
|
||
|
form => '<type> <args>',
|
||
|
args => qr{^(\w+)(?:\s+(.*))?$}i,
|
||
|
},
|
||
|
|
||
|
unsubscribe => {
|
||
|
help => "Unsubscribe from server events",
|
||
|
form => '<type>',
|
||
|
args => qr{^(\w+)$},
|
||
|
},
|
||
|
|
||
|
handlers => {
|
||
|
help => "List the event handlers registered with the server.",
|
||
|
form => '[<type>]',
|
||
|
args => qr{^(\w+)?$},
|
||
|
},
|
||
|
|
||
|
debuglevel => {
|
||
|
help => "Get/set the debugging level of the server.",
|
||
|
form => '[<level>]',
|
||
|
args => qr{^([0-5])?$},
|
||
|
},
|
||
|
|
||
|
help => {
|
||
|
help => "show list of commands or help for a particular command, if given.",
|
||
|
form => "[<command>]",
|
||
|
args => qr{^(\w+)?$},
|
||
|
},
|
||
|
|
||
|
### Internal/debugging commands
|
||
|
timedbuffer => { args => qr{^$} },
|
||
|
|
||
|
);
|
||
|
|
||
|
# Pattern to match command words
|
||
|
$CommandPattern = join '|', keys %CommandTable;
|
||
|
$CommandPattern = qr{^($CommandPattern)$};
|
||
|
}
|
||
|
|
||
|
|
||
|
### (CONSTRUCTOR) METHOD: new( $server=JobServer, $socket=IO::Socket )
|
||
|
### Create a new JobServer::Client object for the given I<socket> and I<server>.
|
||
|
sub new {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $server = shift or confess "no server argument";
|
||
|
my $sock = shift or confess "no socket argument";
|
||
|
|
||
|
$self = fields::new( $self ) unless ref $self;
|
||
|
$self->SUPER::new( $sock );
|
||
|
|
||
|
$self->{server} = $server;
|
||
|
$self->{state} = 'new';
|
||
|
|
||
|
return $self;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: state( [$newstate] )
|
||
|
### Get/set the client's state message.
|
||
|
sub state {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = shift if @_;
|
||
|
return $self->{state};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: stringify( undef )
|
||
|
### Return a string representation of the client object.
|
||
|
sub stringify {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
return sprintf( '%s:%d',
|
||
|
$self->{sock}->peerhost,
|
||
|
$self->{sock}->peerport );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: event_read( undef )
|
||
|
### Readable event callback -- read input from the client and append it to the
|
||
|
### read buffer. Then peel lines off the read buffer and send them to the line
|
||
|
### processor.
|
||
|
sub event_read {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
my $bref = $self->read( 1024 );
|
||
|
|
||
|
if ( !defined $bref ) {
|
||
|
$self->close;
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
$self->{read_buf} .= $$bref;
|
||
|
|
||
|
while ($self->{read_buf} =~ s/^(.+?)\r?\n//) {
|
||
|
$self->processLine( $1 );
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: close( undef )
|
||
|
### Close the client connection after unregistering from the server --
|
||
|
### overridden from Danga::Socket.
|
||
|
sub close {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{server}->disconnectClient( $self ) if $self->{server};
|
||
|
$self->SUPER::close;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: sock( undef )
|
||
|
### Return the IO::Socket object that corresponds to this client.
|
||
|
sub sock {
|
||
|
my JobServer::Client $self = shift;
|
||
|
return $self->{sock};
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: sock( undef )
|
||
|
### Return the file descriptor that is associated with the IO::Socket object
|
||
|
### that corresponds to this client.
|
||
|
sub fdno {
|
||
|
my JobServer::Client $self = shift;
|
||
|
return fileno( $self->{sock} );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: event_err( undef )
|
||
|
### Handle Danga::Socket error events.
|
||
|
sub event_err {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->close;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: event_hup( undef )
|
||
|
### Handle Danga::Socket hangup events.
|
||
|
sub event_hup {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->close;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: debugMsg( $level, $format, @args )
|
||
|
### Send a debugging message to the server.
|
||
|
sub debugMsg {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->{server}->debugMsg( @_ );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: logMsg( $type, $format, @args )
|
||
|
### Send a log message to the server.
|
||
|
sub logMsg {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->{server}->logMsg( @_ );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: processLine( $line )
|
||
|
### Command dispatcher -- parse I<line> as a command and dispatch it to the
|
||
|
### correct command handler method. The class-global %CommandTable contains the
|
||
|
### dispatch table for this method.
|
||
|
sub processLine {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $line = shift or return undef;
|
||
|
|
||
|
my (
|
||
|
$cmd, # Command word
|
||
|
$args, # Argument string from user
|
||
|
$cmdinfo, # Command hashref
|
||
|
@args, # Parsed arguments
|
||
|
$method, # Command method to call
|
||
|
);
|
||
|
|
||
|
# Split the line into command and argument string
|
||
|
( $cmd, $args ) = split /\s+/, $line, 2;
|
||
|
$args = '' if !defined $args;
|
||
|
|
||
|
$self->debugMsg( 5, "Matching '%s' against command table pattern %s",
|
||
|
$cmd, $CommandPattern );
|
||
|
|
||
|
# If it's a command in the command table, dispatch to the appropriate
|
||
|
# command handler after parsing any arguments.
|
||
|
if ( $cmd =~ $CommandPattern ) {
|
||
|
$method = "cmd_$1";
|
||
|
$cmdinfo = $CommandTable{ $1 };
|
||
|
|
||
|
# Parse command arguments
|
||
|
if ( @args = ($args =~ $cmdinfo->{args}) ) {
|
||
|
|
||
|
# If the pattern didn't contain captures, throw away the args
|
||
|
@args = () unless ( @+ > 1 );
|
||
|
|
||
|
eval { $self->$method(@args) };
|
||
|
if ( $@ ) { $self->errorResponse($@) }
|
||
|
}
|
||
|
|
||
|
# Valid command, but bad args
|
||
|
else {
|
||
|
$self->errorResponse( "Usage: $cmd " . $cmdinfo->{form} );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Invalid command
|
||
|
else {
|
||
|
$self->errorResponse( "Invalid command '$cmd'" );
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: okayResponse( @msg )
|
||
|
### Set an 'OK' response string made up of the I<msg> parts concatenated
|
||
|
### together.
|
||
|
sub okayResponse {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $msg = join( '', @_ );
|
||
|
|
||
|
1 while chomp( $msg );
|
||
|
|
||
|
$self->debugMsg( 3, "[Client %s:%d] OK: %s",
|
||
|
$self->{sock}->peerhost,
|
||
|
$self->{sock}->peerport,
|
||
|
$msg,
|
||
|
);
|
||
|
|
||
|
$self->write( "OK $msg\r\n" );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: errorResponse( @msg )
|
||
|
### Send an 'ERR' response string made up of the I<msg> parts concatenated
|
||
|
### together.
|
||
|
sub errorResponse {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $msg = join( '', @_ );
|
||
|
|
||
|
# Trim newlines off the end of the message
|
||
|
1 while chomp( $msg );
|
||
|
|
||
|
$self->logMsg( "error", "[Client %s:%d] ERR: %s",
|
||
|
$self->{sock}->peerhost,
|
||
|
$self->{sock}->peerport,
|
||
|
$msg,
|
||
|
);
|
||
|
|
||
|
$msg =~ s{at \S+ line \d+\..*}{};
|
||
|
$self->write( "ERR $msg\r\n" );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: multilineResponse( $msg, @lines )
|
||
|
### Send an 'OK' response containing the given I<msg> followed by one or more
|
||
|
### I<lines> of a multi-line response followed by an 'END'.
|
||
|
sub multilineResponse {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my ( $msg, @lines ) = @_;
|
||
|
|
||
|
chomp( @lines );
|
||
|
$msg =~ s{:\s*$}{};
|
||
|
|
||
|
$self->okayResponse( "$msg:" );
|
||
|
$self->write( join("\r\n", @lines, "END") . "\r\n" );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: eventMessage( $type, $msg )
|
||
|
### Send an event notification I<msg> for the given I<type> to the client.
|
||
|
sub eventMessage {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my ( $type, $msg ) = @_;
|
||
|
|
||
|
1 while chomp( $type, $msg );
|
||
|
$self->write( "EVENT {$type} $msg\r\n" );
|
||
|
}
|
||
|
|
||
|
|
||
|
### FUNCTION: stringifyHandlers( \%handlers )
|
||
|
### Stringify a hashref full of handler coderefs.
|
||
|
sub stringifyHandlers {
|
||
|
my $handlers = shift or confess "No handlers argument";
|
||
|
|
||
|
my @rows = ();
|
||
|
|
||
|
foreach my $key ( keys %$handlers ) {
|
||
|
if ( ref $handlers->{$key} eq 'HASH' ) {
|
||
|
push( @rows,
|
||
|
" $key => {",
|
||
|
map { " $_" } stringifyHandlers($handlers->{$key}),
|
||
|
"}" );
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
push @rows, sprintf('%s -> %s', $key, $handlers->{$key});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return @rows;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
#####################################################################
|
||
|
### C O M M A N D M E T H O D S
|
||
|
#####################################################################
|
||
|
|
||
|
### METHOD: cmd_get_job( undef )
|
||
|
### Command handler for the C<get_job> command.
|
||
|
sub cmd_get_job {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'getting job';
|
||
|
my $job = $self->{server}->getJob( $self );
|
||
|
|
||
|
if ( $job ) {
|
||
|
my $jobString = $job->stringify;
|
||
|
$self->{state} = sprintf( 'got job %s', $jobString );
|
||
|
return $self->okayResponse( "JOB ". $jobString );
|
||
|
} else {
|
||
|
$self->{state} = 'idle (no jobs)';
|
||
|
return $self->okayResponse( "IDLE" );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_add_jobs( $argstring )
|
||
|
### Command handler for the C<add_job> command.
|
||
|
sub cmd_add_jobs {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $argstring = shift or return;
|
||
|
|
||
|
# Turn the argument into an array of arrays
|
||
|
my @tuples = map {
|
||
|
JobServer::Job->new( $self, $_ )
|
||
|
} split /\s*,\s*/, $argstring;
|
||
|
|
||
|
$self->{state} = sprintf 'adding %d jobs', scalar @tuples;
|
||
|
my @responses = $self->{server}->addJobs( @tuples );
|
||
|
$self->{state} = 'idle';
|
||
|
|
||
|
return $self->multilineResponse( "Done", @responses );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_source_counts( undef )
|
||
|
### Command handler for the C<source_counts> command.
|
||
|
sub cmd_source_counts {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->{state} = 'source counts';
|
||
|
|
||
|
my %counts = $self->{server}->getJobCounts;
|
||
|
my @lines = map { sprintf '%4d: %d', $_, $counts{$_} } sort keys %counts;
|
||
|
|
||
|
return $self->multilineResponse( 'Source counts:', @lines );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_stop_moves( undef )
|
||
|
### Command handler for the C<stop_moves> command.
|
||
|
sub cmd_stop_moves {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $allFlag = shift || '';
|
||
|
|
||
|
$self->{state} = 'stop moves';
|
||
|
my $msg;
|
||
|
|
||
|
if ( $allFlag ) {
|
||
|
$msg = $self->{server}->stopAllJobs( $self );
|
||
|
} else {
|
||
|
$msg = $self->{server}->stopNewJobs( $self );
|
||
|
}
|
||
|
|
||
|
$self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_is_moving( undef )
|
||
|
### Command handler for the C<is_moving> command.
|
||
|
sub cmd_is_moving {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $userid = shift or croak "No userid";
|
||
|
|
||
|
$self->{state} = 'is moving';
|
||
|
$self->debugMsg( 2, "Checking to see if user %d is moving.", $userid );
|
||
|
|
||
|
my $job = $self->{server}->getJobForUser( $userid );
|
||
|
my $msg;
|
||
|
|
||
|
if ( $job ) {
|
||
|
$self->debugMsg( 3, "is_moving: Got a job for userid $userid" );
|
||
|
$msg = "1";
|
||
|
} else {
|
||
|
$self->debugMsg( 3, "is_moving: No job for userid $userid" );
|
||
|
$msg = "0";
|
||
|
}
|
||
|
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
### METHOD: cmd_list_jobs( undef )
|
||
|
### Command handler for the C<list_jobs> command.
|
||
|
sub cmd_list_jobs {
|
||
|
my JobServer::Client $self = shift;
|
||
|
$self->{state} = 'list jobs';
|
||
|
|
||
|
my $stats = $self->{server}->getJobList;
|
||
|
|
||
|
return $self->multilineResponse(
|
||
|
"Joblist:",
|
||
|
"Queued Jobs",
|
||
|
@{$stats->{queued_jobs}},
|
||
|
"",
|
||
|
"Assigned Jobs",
|
||
|
@{$stats->{assigned_jobs}},
|
||
|
"",
|
||
|
@{$stats->{footer}},
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_move_stats( undef )
|
||
|
### Command handler for the C<move_stats> command.
|
||
|
sub cmd_move_stats {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
my (
|
||
|
@jobs, # Recently-finished job objects
|
||
|
%times, # Per-cluster/global time sums
|
||
|
%counts, # Per-cluster job counts
|
||
|
$totaltime,
|
||
|
$totalcount,
|
||
|
@averages, # Average 'alive' times
|
||
|
@stats, # Statistic lines
|
||
|
);
|
||
|
|
||
|
$self->{state} = 'move_stats';
|
||
|
@jobs = $self->{server}->recentmoves
|
||
|
or return $self->multilineResponse( "Move stats:", "No finished jobs" );
|
||
|
|
||
|
$totaltime = 0;
|
||
|
$totalcount = 0;
|
||
|
|
||
|
# Build average 'alive' times
|
||
|
foreach my $job ( @jobs ) {
|
||
|
$times{ $job->srcclusterid } += $job->aliveTime;
|
||
|
$totaltime += $job->aliveTime;
|
||
|
$counts{ $job->srcclusterid }++;
|
||
|
$totalcount++;
|
||
|
}
|
||
|
|
||
|
# Generate averages
|
||
|
@averages = map {
|
||
|
sprintf( ' c%d: %d @ %0.2fs, %0.2fs avg.',
|
||
|
$_, $counts{$_}, $times{$_},
|
||
|
$times{$_} / $counts{$_} )
|
||
|
} sort keys %times;
|
||
|
push @averages,
|
||
|
sprintf( ' total: %d @ %0.2fs, %0.2fs avg.',
|
||
|
$totalcount, $totaltime,
|
||
|
$totaltime / $totalcount );
|
||
|
|
||
|
# Return the statistics
|
||
|
return $self->multilineResponse(
|
||
|
"Move stats:",
|
||
|
"Average 'alive' times (create->finish)",
|
||
|
@averages,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_recent_moves( undef )
|
||
|
### Command handler for the C<recent_moves> command.
|
||
|
sub cmd_recent_moves {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'recent_moves';
|
||
|
|
||
|
my @jobs = $self->{server}->recentmoves;
|
||
|
|
||
|
return $self->multilineResponse( "Recent moves",
|
||
|
map { $_->prettyString } @jobs );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_set_rate( undef )
|
||
|
### Command handler for the C<set_rate> command.
|
||
|
sub cmd_set_rate {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my ( $clusterid, $rate ) = @_;
|
||
|
|
||
|
my $msg;
|
||
|
|
||
|
# Global rate
|
||
|
if ( ! defined $rate ) {
|
||
|
$rate = $clusterid;
|
||
|
$self->{state} = "set global rate";
|
||
|
$msg = $self->{server}->setGlobalRateLimit( $rate );
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
$self->{state} = "set rate for cluster $clusterid";
|
||
|
$msg = $self->{server}->setClusterRateLimit( $clusterid, $rate );
|
||
|
}
|
||
|
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_show_rates( undef )
|
||
|
### Command handler for the C<show_rates> command.
|
||
|
sub cmd_show_rates {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'show_rates';
|
||
|
my %rules = $self->{server}->raterules;
|
||
|
my @lines = map { sprintf '%6s: %2d', $_, $rules{$_} } sort keys %rules;
|
||
|
|
||
|
# If there's no global rate set, show the configured default
|
||
|
unless ( exists $rules{global} ) {
|
||
|
push @lines, "default: " . $self->{server}->defaultRate;
|
||
|
}
|
||
|
|
||
|
$self->multilineResponse( 'Cluster rate limit rules', @lines );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_reset_rate( undef )
|
||
|
### Command handler for the C<reset_rate> command.
|
||
|
sub cmd_reset_rate {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $srcclusterid = shift || '';
|
||
|
|
||
|
$self->{state} = 'reset_rate';
|
||
|
my ( $rval, $msg );
|
||
|
|
||
|
if ( $srcclusterid ) {
|
||
|
$rval = $self->{server}->resetClusterRateLimit( $srcclusterid );
|
||
|
$msg = "Reset rate limit for cluster $srcclusterid to $rval";
|
||
|
} else {
|
||
|
$rval = $self->{server}->resetGlobalRateLimit;
|
||
|
$msg = "Reset global rate limit to $rval";
|
||
|
}
|
||
|
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_finish( undef )
|
||
|
### Command handler for the C<finish> command.
|
||
|
sub cmd_finish {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $spec = shift or confess "No job specification";
|
||
|
$self->{state} = 'finish';
|
||
|
|
||
|
my ( $userid, $srcclusterid, $dstclusterid ) = split /:/, $spec, 3;
|
||
|
|
||
|
my $msg = $self->{server}->requestJobFinish( $self, $userid, $srcclusterid,
|
||
|
$dstclusterid );
|
||
|
|
||
|
if ( $msg ) {
|
||
|
return $self->okayResponse( $msg );
|
||
|
} else {
|
||
|
return $self->errorResponse( "Abort" );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_help( undef )
|
||
|
### Command handler for the C<help> command.
|
||
|
sub cmd_help {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $command = shift || '';
|
||
|
|
||
|
$self->{state} = 'help';
|
||
|
my @response = ();
|
||
|
|
||
|
# Either show help for a particular command
|
||
|
if ( $command && exists $CommandTable{$command} ) {
|
||
|
my $cmdinfo = $CommandTable{ $command };
|
||
|
$cmdinfo->{form} ||= ''; # Non-existant form means no args
|
||
|
|
||
|
@response = (
|
||
|
"--- $command -----------------------------------",
|
||
|
"",
|
||
|
" $command $cmdinfo->{form}",
|
||
|
"",
|
||
|
$cmdinfo->{help} || "(undocumented)",
|
||
|
"",
|
||
|
"Pattern:",
|
||
|
" $cmdinfo->{args}",
|
||
|
"",
|
||
|
);
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
my @cmds = map { " $_" }
|
||
|
grep { exists $CommandTable{$_}{help} }
|
||
|
sort keys %CommandTable;
|
||
|
|
||
|
@response = (
|
||
|
"Available commands:",
|
||
|
"",
|
||
|
@cmds,
|
||
|
"",
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return $self->multilineResponse( "Help:", @response );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_lock( $userid )
|
||
|
### Command handler for the (debugging) C<lock> command.
|
||
|
sub cmd_lock {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $userid = shift;
|
||
|
|
||
|
# Fetch the job for the requested user if possible
|
||
|
my $job = $self->{server}->getJobForUser( $userid )
|
||
|
or return $self->errorResponse( "No such user '$userid'." );
|
||
|
|
||
|
if ( $job->isPrelocked ) {
|
||
|
my $msg = sprintf( "User %d already locked for %d seconds.",
|
||
|
$userid, $job->secondsSinceLock );
|
||
|
return $self->errorResponse( $msg );
|
||
|
}
|
||
|
|
||
|
# Try to lock the user
|
||
|
my $time = $job->prelock;
|
||
|
if ( $time ) {
|
||
|
my $msg = "User $userid locked at: $time (". scalar localtime($time) .")";
|
||
|
return $self->okayResponse( $msg );
|
||
|
} else {
|
||
|
return $self->errorResponse( "Prelocking of user $userid failed." );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_clients( undef )
|
||
|
### Command handler for the C<clients> command.
|
||
|
sub cmd_clients {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'list clients';
|
||
|
|
||
|
my @lines = map {
|
||
|
sprintf '%3d: %s', $_->fdno, $_->state;
|
||
|
} $self->{server}->clients;
|
||
|
|
||
|
return $self->multilineResponse( 'Clients: ', @lines );
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### METHOD: cmd_subscribe( $type, $args )
|
||
|
### Command handler for the C<subscribe> command.
|
||
|
sub cmd_subscribe {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my ( $type, $args ) = @_;
|
||
|
|
||
|
$self->{state} = "subscribe to $type events";
|
||
|
|
||
|
my $msg = $self->{server}->subscribe( $self, $type, $args );
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
### METHOD: cmd_unsubscribe( $type )
|
||
|
### Command handler for the C<unsubscribe> command.
|
||
|
sub cmd_unsubscribe {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my ( $type ) = @_;
|
||
|
|
||
|
$self->{state} = 'unsubscribe from %s events';
|
||
|
|
||
|
my $msg = $self->{server}->unsubscribe( $self, $type );
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_handlers( [$type] )
|
||
|
### Command handler for the C<handlers> command.
|
||
|
sub cmd_handlers {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $type = shift || '';
|
||
|
|
||
|
$self->{state} = 'handlers';
|
||
|
|
||
|
my $handlers = $self->{server}->handlers( $type );
|
||
|
my @res;
|
||
|
|
||
|
if ( $handlers ) {
|
||
|
@res = stringifyHandlers( $handlers );
|
||
|
} else {
|
||
|
@res = ("No handlers registered.");
|
||
|
}
|
||
|
|
||
|
$self->multilineResponse( "Handlers:", @res );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_quit( undef )
|
||
|
### Command handler for the C<quit> command.
|
||
|
sub cmd_quit {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'quitting';
|
||
|
|
||
|
$self->okayResponse( "Goodbye" );
|
||
|
$self->close;
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_debuglevel( [$newLevel] )
|
||
|
### Command handler for the C<debuglevel> command.
|
||
|
sub cmd_debuglevel {
|
||
|
my JobServer::Client $self = shift;
|
||
|
my $level = shift;
|
||
|
|
||
|
$self->{state} = 'debuglevel';
|
||
|
my $msg = '';
|
||
|
|
||
|
if ( defined $level ) {
|
||
|
my $oldLevel = $self->{server}->debugLevel;
|
||
|
my $newLevel = $self->{server}->debugLevel( $level );
|
||
|
$msg = "Debug level was $oldLevel; now $newLevel";
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
$msg = "Debug level is " . $self->{server}->debugLevel;
|
||
|
}
|
||
|
|
||
|
return $self->okayResponse( $msg );
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_shutdown( undef )
|
||
|
### Command handler for the C<shutdown> command.
|
||
|
sub cmd_shutdown {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'shutdown';
|
||
|
|
||
|
my $msg = $self->{server}->shutdown( $self );
|
||
|
$self->{server} = undef;
|
||
|
$self->okayResponse( $msg );
|
||
|
$self->close;
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
### METHOD: cmd_timedbuffer( undef )
|
||
|
### Command handler for the C<timedbuffer> command. FOR DEBUGGING ONLY.
|
||
|
sub cmd_timedbuffer {
|
||
|
my JobServer::Client $self = shift;
|
||
|
|
||
|
$self->{state} = 'timedbuffer';
|
||
|
my @jobs = $self->{server}->recentmoves;
|
||
|
|
||
|
my $count = 1;
|
||
|
my @entries = map { sprintf '%3d. %s', $count++, $_->prettyString } @jobs;
|
||
|
return $self->multilineResponse( "Server's timedbuffer:", @entries );
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
### Template for new command handlers:
|
||
|
|
||
|
# ### METHOD: cmd_foo( undef )
|
||
|
# ### Command handler for the C<foo> command.
|
||
|
# sub cmd_foo {
|
||
|
# my JobServer::Client $self = shift;
|
||
|
#
|
||
|
# $self->{state} = 'foo';
|
||
|
# return $self->errorResponse( "Not yet implemented." );
|
||
|
# }
|
||
|
#
|
||
|
#
|
||
|
|
||
|
1;
|
||
|
|
||
|
# Local Variables:
|
||
|
# mode: perl
|
||
|
# c-basic-indent: 4
|
||
|
# indent-tabs-mode: nil
|
||
|
# End:
|