# $Id: Base.pm 105 2006-04-26 09:12:24Z daisuke $ # # Copyright (c) 2005 Daisuke Maki # All rights reserved. package Xango::Broker::Base; use strict; use HTTP::Request; use POE; use Xango; use Xango::Config; use Xango::Job; our $AUTOLOAD; use constant DEFAULT_HTTP_COMP_CLASS => 'POE::Component::Client::HTTP'; use constant DEFAULT_DNS_COMP_CLASS => 'POE::Component::Client::DNS'; use constant DEFAULT_HTTP_COMP_ARGS => []; use constant DEFAULT_DNS_CACHE_CLASS => 'Cache::FileCache'; use constant DEFAULT_DNS_CACHE_ARGS => { namespace => 'DNS', default_expires_in => 3 * 3600 }; sub new { my $class = shift; my $self = bless {}, $class; $self->initialize(@_); return $self; } sub initialize { my $self = shift; my %args = @_; if ($args{ConfigFile}) { my $config = Xango::Config->init($args{ConfigFile}); $self->_load_config($config); } # Set values provided by %args. This overrides the config file $self->{ALIAS} = $args{Alias} if exists $args{Alias}; $self->{DNS_CACHE_ARGS} = $args{DnsCacheArgs} if exists $args{DnsCacheArgs}; $self->{DNS_CACHE_CLASS} = $args{DnsCacheClass} if exists $args{DnsCacheClass}; $self->{DNS_CACHE_ARGS_DEREF} = $args{DnsCacheArgsDeref} if $args{DnsCacheArgsDeref}; $self->{DNS_COMP_ALIAS} = $args{DnsCompAlias} if $args{DnsCompAlias}; $self->{DNS_COMP_CLASS} = $args{DnsCompClass} if $args{DnsCompClass}; $self->{HANDLER_ALIAS} = $args{HandlerAlias} if $args{HandlerAlias}; $self->{HTTP_COMP_CLASS} = $args{HttpCompClass} if $args{HttpCompClass}; $self->{HTTP_COMP_ARGS} = $args{HttpCompArgs} if $args{HttpCompArgs}; $self->{MAX_HTTP_COMP} = $args{MaxHttpComp} if $args{MaxHttpComp}; $self->{ENABLE_DNS_CACHE} = exists $args{EnableDnsCache} ? $args{EnableDnsCache} : 1; # Set defaults. $self->{ALIAS} ||= 'broker'; $self->{CONFIG_FILE} ||= undef; $self->{DNS_CACHE} ||= undef; $self->{DNS_CACHE_CLASS} ||= DEFAULT_DNS_CACHE_CLASS; $self->{DNS_CACHE_ARGS} ||= DEFAULT_DNS_CACHE_ARGS; if (!exists $self->{DNS_CACHE_ARGS_DEREF}) { $self->{DNS_CACHE_ARGS_DEREF} = 0; } $self->{DNS_COMP_ALIAS} ||= 'dns_resolver'; $self->{DNS_COMP_CLASS} ||= DEFAULT_DNS_COMP_CLASS; $self->{DNS_PENDING} = {}; # Non-configurable $self->{FETCHERS} = {}; # Non-configurable $self->{HANDLER_ALIAS} ||= 'handler'; $self->{HTTP_COMP_CLASS} ||= DEFAULT_HTTP_COMP_CLASS; $self->{HTTP_COMP_ARGS} ||= []; $self->{JOBS_PENDING} = []; # Non-configurable $self->{MAX_HTTP_COMP} ||= 10; $self->{SHUTDOWN} = 0; # Non-configurable $self->{STRICT_JOB_TYPE} ||= 'Xango::Job'; $self->{UNDEF_ON_FINALIZE} ||= 0; my $k_munge; foreach my $k (keys %args) { $k_munge = $k; $k_munge =~ s/([a-z])([A-Z])/$1_$2/g; $self->{uc ($k_munge)} = $args{$k}; } } sub _load_config { my($self, $config) = @_; $self->{ALIAS} = $config->param('BrokerAlias'); my $set = sub { my $p = shift; my $c = $p; $c =~ s/([a-z])([A-Z])/$1_$2/g; $c = uc $c; $self->{$c} = $config->param($p); }; $set->('DnsCacheClass'); $set->('DnsCacheArgs'); $set->('DnsCacheArgsDeref'); $set->('DnsCompAlias'); $set->('DnsCompClass'); $set->('HandlerAlias'); $set->('HttpCompArgs'); $set->('HttpCompClass'); $set->('MaxHttpComp'); } sub states { my $self = shift; return ( object_states => [ $self => [ '_start', '_stop', 'create_dns_cache', 'create_http_request', 'create_http_comp_data', 'dispatch_to_lightest_load', 'dispatch_http_fetch', 'finalize_job', 'fake_error_response', 'handle_signal', 'handle_dns_response', 'handle_http_response', 'install_sighandlers', 'register_http_request', 'register_dns_request', 'signal_http_comp', 'spawn_dns_comp', 'spawn_http_comp', 'unregister_http_request', 'shutdown_broker', ] ] ); } sub attr { my $self = shift; my $field = shift; $field =~ s/([a-z])([A-Z])/$1_$2/g; $field = uc $field; my $attr = $self->{$field}; if (@_) { $self->{$field} = shift; } return $attr; } sub spawn { my $class = shift; my $self = $class->new(@_); POE::Session->create( heap => $self, $self->states, ); return $self; } sub check_job_type { my $self = shift; if (my $type = $self->strict_job_type) { return eval { $_[0]->isa($type) }; } return 1; } sub _start { my($kernel, $session, $obj) = @_[KERNEL, SESSION, OBJECT]; $kernel->alias_set($obj->alias); # Install signal handlers $kernel->call($session, 'install_sighandlers'); # Spawn a DNS component to resolve hostnames $kernel->call($session, 'spawn_dns_comp'); # create DNS cache, if specified. if ($obj->enable_dns_cache) { my $cache = $kernel->call($session, 'create_dns_cache'); $obj->attr(dns_cache => $cache); } $kernel->call( $session, 'spawn_http_comp', $obj->max_http_comp > 10 ? 10 : $obj->max_http_comp ); } sub _stop { my($kernel, $heap, $obj) = @_[KERNEL, HEAP, OBJECT]; Xango::debug("[_stop]: broker session stopping."); $kernel->alias_remove($obj->alias); } sub shutdown_broker { my($obj) = @_[OBJECT]; $obj->shutdown(1); } sub spawn_dns_comp { my($kernel, $object) = @_[KERNEL, OBJECT]; my $class = $object->dns_comp_class(); my $alias = $object->dns_comp_alias(); eval "require $class"; die if $@; $class->spawn(Alias => $alias, Timeout => 45); } sub spawn_http_comp { my($kernel, $session, $obj, $how_many) = @_[KERNEL, SESSION, OBJECT, ARG0]; Xango::debug("[spawn_http_comp]: Spawn $how_many HTTP components"); my $fetchers = $obj->fetchers; my $class = $obj->http_comp_class; my $args = $obj->http_comp_args; eval "require $class"; die if $@; for (1..$how_many) { my $alias = join('-', 'http', time(), rand()); $class->spawn(@$args, Alias => $alias); $fetchers->{$alias} = $kernel->call($session, 'create_http_comp_data', $alias); } } sub create_http_comp_data { return {}; } sub signal_http_comp { my($kernel, $http_comp) = @_[KERNEL, ARG0]; $kernel->signal($http_comp, 'INT'); } sub handle_signal { my($kernel, $obj, $sig) = @_[KERNEL, OBJECT, ARG0]; if ($sig eq 'PIPE') { # ignore return; } Xango::debug("[handle_signal]: Received signal $sig. Setting shutdown state"); $obj->shutdown(1); $kernel->sig_handled(); } sub install_sighandlers { my($kernel) = @_[KERNEL]; $kernel->sig(PIPE => 'handle_signal'); $kernel->sig(INT => 'handle_signal'); $kernel->sig(QUIT => 'handle_signal'); $kernel->sig(TERM => 'handle_signal'); $kernel->sig(HUP => 'reload_config'); } sub create_dns_cache { my ($kernel, $obj) = @_[KERNEL, OBJECT]; my $class = $obj->dns_cache_class; my $args = $obj->dns_cache_args; my $deref = $obj->dns_cache_args_deref; eval "require $class"; die if $@; return $deref ? $class->new( ref($args) eq 'HASH' ? %$args : ref($args) eq 'ARRAY' ? @$args : die "DnsCacheArgs could not be dereferenced to an array or hash" ) : $class->new($args); } sub create_http_request { my($kernel, $session, $obj, $job) = @_[KERNEL, SESSION, OBJECT, ARG0]; my $uri = $job->uri->clone; $uri->host($job->notes('host_ip')); my $req = HTTP::Request->new(GET => $uri); $req->header(Host => $job->notes('host_name')); # Give the handler a chance to munge with the request $kernel->call($obj->handler_alias, 'prep_request', $job, $req); return $req; } sub register_dns_request { my($obj, $host, $job) = @_[OBJECT, ARG0, ARG1]; my $queue = $obj->dns_pending(); $queue->{$host} ||= []; push @{$queue->{$host}}, $job; } sub register_http_request { my($obj, $fetcher_id, $job) = @_[OBJECT, ARG0, ARG1]; # We need to register that there was a new request to this fetcher my $fetchers = $obj->fetchers(); my $data = $fetchers->{$fetcher_id}; $data->{last_request_time} = time(); $data->{jobs}->{$job->id} = $job; } sub unregister_http_request { my($obj, $fetcher_id, $job) = @_[OBJECT, ARG0, ARG1]; my $fetchers = $obj->fetchers(); my $data = $fetchers->{$fetcher_id}; $data->{last_request_time} = time(); delete $data->{jobs}->{$job->id}; } sub dispatch_to_lightest_load { my($kernel, $session, $obj, $job) = @_[KERNEL, SESSION, OBJECT, ARG0]; # Find the http component with the least load. my $min = undef; my $fetcher_id; while (my($id, $data) = each %{$obj->fetchers}) { my $size = scalar keys %{$data->{jobs}}; if (!defined $min || $min > $size) { $fetcher_id = $id; $min = $size; last unless $min; } } if ($fetcher_id) { $job->notes(fetcher => $fetcher_id); $kernel->call($session, 'dispatch_http_fetch', $job); } } sub dispatch_http_fetch { my($kernel, $session, $obj, $job) = @_[KERNEL, SESSION, OBJECT, ARG0]; my $uri = $job->uri; Xango::debug("[dispatch_http_fetch]: dispatch_http_fetch $uri"); # Apply policy if (! $kernel->call($obj->handler_alias, 'apply_policy', $job)) { return $kernel->call($session, 'finalize_job', $job); } # Resolve this URI first (check flags for sanity. we don't want to # get into an infinite loop or something) my $host_ip = $job->notes('host_ip'); if (!$host_ip) { if ($uri->host =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { $host_ip = $uri->host; $job->notes(host_name => $uri->host); $job->notes(host_ip => $uri->host); } elsif (! $job->notes('dns_resolved') && $uri->host !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { my $host = $uri->host; my $dnscache = $obj->dns_cache(); $host_ip = $dnscache ? $dnscache->get($host) : undef; if ($host_ip) { $job->notes(dns_resolved => 1); $job->notes(host_name => $host); $job->notes(host_ip => $host_ip); } else { $kernel->call($session, 'register_dns_request', $host, $job); return $kernel->post( $obj->dns_comp_alias, 'resolve', 'handle_dns_response', $host ); } # We cache failed responses as well. Check if this is such case if ($host_ip eq '0.0.0.0') { my $err = "Could not connect to " . "$uri (No address associated with $host (CACHED))"; return $kernel->yield('fake_error_response', $job, $err); } } } # At this point, either the uri that was requested was already an IP # address, or we have successfully resolved the hostname. if (! $host_ip || $host_ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { Xango::debug(sprintf(<yield('finalize_job', $job); } # Create a new HTTP::Request object. my $request = $kernel->call($session, 'create_http_request', $job); my $fetcher = $job->notes('fetcher'); # Register to the fetcher that there was a request. $kernel->call($session, 'register_new_request', $fetcher, $job); my @request_args = ($fetcher, 'request', 'handle_http_response', $request, $job); if (&Xango::DEBUG) { push @request_args, 'fetcher_progress'; } # Fire! $kernel->post(@request_args) or die "Failed to post to fetcher ${fetcher}'s request state"; } sub handle_http_response { my($kernel, $session, $obj, $reqpack, $respack) = @_[KERNEL, SESSION, OBJECT, ARG0, ARG1]; my $job = $reqpack->[1]; my $uri = $job->uri; Xango::debug("[handle_http_response]: $uri"); # Immediately unregister this request $kernel->call($session, 'unregister_http_request', $job->notes('fetcher'), $job); # put the uri back to hostname, if available if ($job->notes('host_name')) { $uri->host($job->notes('host_name')); } my $response = $respack->[0]; my $request = $reqpack->[0]; $job->notes(http_response => $response); if ($request->uri ne $uri) { # probably DNS $request->uri($uri); } $response->request($request); $kernel->post($obj->handler_alias, 'handle_response', $job) or die "Failed to post event 'handle_response' to '" . $obj->handler_alias . "'"; $kernel->yield('finalize_job', $job); } sub fake_error_response { my($kernel, $job, $message) = @_[KERNEL, ARG0, ARG1]; my $response = HTTP::Response->new(500); $response->content(< An Error Occurred

An Error Occurred

500 $message new(GET => $job->uri); $request->content('DUMMY-REQUEST'); $response->request($request); $kernel->yield('handle_http_response', [ $request, $job ], [ $response ]); } sub DESTROY {} sub AUTOLOAD { my $self = $_[0]; goto UNDEFINED unless ref($self) ne 'HASH'; my $method = uc $AUTOLOAD; $method =~ s/^.+::([^:]+)$/$1/; goto UNDEFINED unless exists $self->{$method}; my $code = sprintf(<<' EOSQL', $AUTOLOAD, $method, $method); sub %s { my $self = shift; my $ret = $self->{%s}; if (@_) { $self->{%s} = shift; } return $ret; } EOSQL eval $code; die if $@; goto &$AUTOLOAD; UNDEFINED: Carp::croak("Undefined subroutine $AUTOLOAD"); } sub finalize_job { my($kernel, $session, $obj, $job) = @_[KERNEL, SESSION, OBJECT, ARG0]; my $fetcher_id = $job->notes('fetcher'); $kernel->call($obj->handler_alias, 'finalize_job', $job); $kernel->call($session, 'unregister_http_request', $fetcher_id, $job); if ($obj->undef_on_finalize) { undef $job; } Xango::debug("[finalize_job]: fetcher $fetcher_id done"); } sub handle_dns_response { my($kernel, $obj) = @_[KERNEL, OBJECT]; my $request_address = $_[ARG0]->[0]; my $response_object = $_[ARG1]->[0]; my $response_error = $_[ARG1]->[1]; my $pending = $obj->dns_pending(); my $requests = $pending->{$request_address}; if (!$requests) { Xango::debug("Resolved $request_address, but no requests associated to it were found!"); return undef; } if (defined $response_object) { my $ip; foreach my $answer ($response_object->answer()) { next unless $answer->type eq 'A'; $ip = $answer->rdatastr; last; } if ($ip) { Xango::debug("[handle_dns_response]: $request_address resolved to $ip."); $obj->dns_cache->set($request_address, $ip) if $obj->dns_cache; foreach my $job (@$requests) { $job->notes(host_name => $request_address); $job->notes(host_ip => $ip); $kernel->yield('dispatch_http_fetch', $job); } return 1; # yay } } # If we got here, everything else is an error. # XXX - Make sure that fetcher->{dispatched} is unset if (!defined $response_object) { # No response - create a fake 500 response, short-cicuit and send # the result to the job processor foreach my $job (@$requests) { # XXX - Rethink what this does. # delete $heap->{FETCHERS}->{$job->{fetcher}}->{dispatched}->{$job->{id}}; $kernel->yield( 'fake_error_response', $job, "Could not connect to " . $job->uri . " ($response_error)" ); } } else { # If we got here, we probably didn't find any 'A' records in the # DNS lookup. Treat this as an error as well foreach my $job (@$requests) { # XXX - Rethink what this does. # delete $heap->{FETCHERS}->{$job->{fetcher}}->{dispatched}->{$job->{id}}; $kernel->yield( 'fake_error_response', $job, "Could not connect to " . $job->uri . " ($response_error)" ); } } $obj->dns_cache->set($request_address, '0.0.0.0') if $obj->dns_cache; return undef; } 1; __END__ =head1 NAME Xango::Broker::Base - Base Class for Xango Broker =head1 SYNOPSIS package MyBroker; use strict; use base qw(Xango::Broker::Base); =head1 DESCRIPTION Xango::Broker::Base implements the common broker methods. You should be using Xango::Broker::Pull or Xango::Broker::Push in your applications. See respective documentations. =head1 OBJECT METHODS =head2 spawn(%args) Create a new Xango::Broker::Base object, and also spawn a new POE::Session using that new object. To start a Xango::Broker session, you should be using spawn(), and not new(). Accepts the same arguments as new(). =head2 new(%args) Create a new Xango::Broker::Base object. Arguments passed to it are stored via initialize() =over 4 =item Alias (scalar) The POE session alias for the broker. =item ConfigFile (scalar) Path to the config file used by Xango::Config. Values in this file will be used as the default value for other variables. =item EnableDnsCache Flag to tell Xango if DNS caching should be used. By default it's on. Turn this off for short-running Xango process that want to avoid repeated DNS failures caused by a cached DNS query failure. =item DnsCacheClass (scalar) Perl class to use for DNS cache. =item DnsCacheArgs (hashref | arrayref) Arguments to the DNS cache constructor =item DnsCacheArgsDeref (boolean) Try to de-reference the DNS cache constructor argument specified by DnsCacheArgs when creating the cache. This is required if your DNS cache class uses simple list values instead of hash/array refs (e.g. Cache::Cache and Cache::FastMmap) =item DnsCompClass (scalar) Specify the POE component class to asynchronously resolve DNS requests. By default POE::Component::Client::DNS is used. =item HandlerAlias (scalar) The POE session alias for the handler. =item HttpCompClass (scalar) Specify the POE component class to handle the actual HTTP fetching. By default POE::Component::Client::HTTP is used. =item HttpCompArgs (hashref | arrayref) Arguments to the HTTP fetching component constructor. =item MaxHttpComp (integer) The number of concurrent http agents (i.e. the number of POE::Component::Client::HTTP sessions) that are allowed. The default is 10, but for anything other than a toy application, something in the order of 50 ~ 100 is the recommended value. Unless this number is less than 10, the broker starts with 10 sessions, and successively grows the pool of agents when there are not enough agents to handle the currently available jobs, until the maximum is reached. If the max is less than 10, the starting number if equal to the max. =back =head2 initialize Initializes the broker object. =head2 attr(key =E $value) General purpose getter/stter for attributes. =head2 dns_comp_class The class name for DNS resolver component. =head2 dns_comp_alias The alias for DNS resolver component. =head2 states Returns the states list as passed to the POE::Session constructor. =head1 STATES States are all called either by yield() or post(). The arguments described below are to be used in that context, i.e. yield('state', $arg1, $arg2...) =head2 check_job_type =head2 create_dns_cache Creates and sets the DNS cache. You probably don't need to worry about this. =head2 create_http_comp_data =head2 create_http_request Creates a new HTTP::Request object to be fetched. =head2 dispatch_http_fetch($job) Start an HTTP fetch for $job. $job must have a HTTP fetcher associated to it. =head2 dispatch_to_lightest_load($job) Choose the HTTP fetcher session with the least load, and call dispath_http_fetch after associating it to a job =head2 fake_error_response Create a fake HTTP error response, which is passed to handle_http_response. This is used to notify internal errors to the handler. =head2 install_sighandlers Installs the global signal handlers. =head2 spawn_dns_comp Spawns the component to resolve DNS lookups. =head2 spawn_http_comp($howmany) =head2 signal_http_comp Send a signal to the specified HTTP component. =head2 shutdown_broker Sets the shutdown flag on the broker. The broker will attempt to shutdown gracefully. =head2 handle_dns_response =head2 handle_http_response =head2 handle_signal Handle a given signal. The signal name is in ARG0. =head2 register_dns_request =head2 register_http_request =head2 unregister_http_request =head2 finalize_job =head1 SEE ALSO L L =head1 AUTHOR Copyright (c) 2005 Daisuke Maki Edmaki@cpan.orgE. All rights reserved. Development funded by Brazil, Ltd. Ehttp://b.razil.jpE =cut