File Coverage

blib/lib/Perlbal/Service.pm
Criterion Covered Total %
statement 332 596 55.7
branch 132 352 37.5
condition 43 140 30.7
subroutine 43 64 67.1
pod 0 48 0.0
total 550 1200 45.8


line stmt bran cond sub pod time code
1             ######################################################################
2             # Service class
3             ######################################################################
4             #
5             # Copyright 2004, Danga Interactive, Inc.
6             # Copyright 2005-2007, Six Apart, Ltd.
7             #
8              
9             package Perlbal::Service;
10 22     22   148 use strict;
  22         57  
  22         1057  
11 22     22   168 use warnings;
  22         51  
  22         896  
12 22     22   120 no warnings qw(deprecated);
  22         46  
  22         896  
13              
14 22     22   21989 use Perlbal::BackendHTTP;
  22         78  
  22         890  
15 22     22   19663 use Perlbal::Cache;
  22         80  
  22         733  
16 22     22   181 use Perlbal::Util;
  22         55  
  22         4124  
17             use fields (
18 22         1429 'name', # scalar: name of this service
19             'role', # scalar: role type 'web_server', 'reverse_proxy', etc...
20             'enabled', # scalar: bool, whether we're enabled or not (enabled = listening)
21              
22             'pool', # Perlbal::Pool that we're using to allocate nodes if we're in proxy mode
23             'listener', # Perlbal::TCPListener object, when enabled
24             'reproxy_cache', # Perlbal::Cache object, when enabled
25              
26             # end-user tunables
27             'listen', # scalar IP:port of where we're listening for new connections
28             'docroot', # document root for webserver role
29             'dirindexing', # bool: directory indexing? (for webserver role) not async.
30             'index_files', # arrayref of filenames to try for index files
31             'enable_concatenate_get', # bool: if user can request concatenated files
32             'enable_put', # bool: whether PUT is supported
33             'enable_md5', # bool: whether Content-MD5 is supported on PUT
34             'max_put_size', # int: max size in bytes of a put file
35             'max_chunked_request_size', # int: max size in bytes of a chunked request (to be written to disk first)
36             'min_put_directory', # int: number of directories required to exist at beginning of URIs in put
37             'enable_delete', # bool: whether DELETE is supported
38             'high_priority_cookie', # cookie name to check if client can 'cut in line' and get backends faster
39             'high_priority_cookie_contents', # aforementioned cookie value must contain this substring
40             'backend_persist_cache', # scalar: max number of persistent backends to hold onto while no clients
41             'persist_client', # bool: persistent connections for clients
42             'persist_backend', # bool: persistent connections for backends
43             'verify_backend', # bool: get attention of backend before giving it clients (using OPTIONS)
44             'verify_backend_path', # path to check with the OPTIONS request (default *)
45             'max_backend_uses', # max requests to send per kept-alive backend (default 0 = unlimited)
46             'connect_ahead', # scalar: number of spare backends to connect to in advance all the time
47             'buffer_size', # int: specifies how much data a ClientProxy object should buffer from a backend
48             'buffer_size_reproxy_url', # int: same as above but for backends that are reproxying for us
49             'queue_relief_size', # int; number of outstanding standard priority
50             # connections to activate pressure relief at
51             'queue_relief_chance', # int:0-100; % chance to take a standard priority
52             # request when we're in pressure relief mode
53             'trusted_upstream_proxies', # Net::Netmask object containing netmasks for trusted upstreams
54             'always_trusted', # bool; if true, always trust upstreams
55             'blind_proxy', # bool: if true, do not modify X-Forwarded-For, X-Host, or X-Forwarded-Host headers
56             'enable_reproxy', # bool; if true, advertise that server will reproxy files and/or URLs
57             'reproxy_cache_maxsize', # int; maximum number of reproxy results to be cached. (0 is disabled and default)
58             'client_sndbuf_size', # int: bytes for SO_SNDBUF
59             'server_process' , # scalar: path to server process (executable)
60             'persist_client_idle_timeout', # int: keep-alive timeout in seconds for clients (default is 30)
61             'idle_timeout', # int: idle timeout outside of keep-alive time (default is 30)
62              
63             # Internal state:
64             'waiting_clients', # arrayref of clients waiting for backendhttp conns
65             'waiting_clients_highpri', # arrayref of high-priority clients waiting for backendhttp conns
66             'waiting_clients_lowpri', # arrayref of low-priority clients waiting for backendhttp conns
67             'waiting_client_count', # number of clients waiting for backends
68             'waiting_client_map' , # map of clientproxy fd -> 1 (if they're waiting for a conn)
69             'pending_connects', # hashref of "ip:port" -> $time (only one pending connect to backend at a time)
70             'pending_connect_count', # number of outstanding backend connects
71             'bored_backends', # arrayref of backends we've already connected to, but haven't got clients
72             'hooks', # hashref: hookname => [ [ plugin, ref ], [ plugin, ref ], ... ]
73             'plugins', # hashref: name => 1
74             'plugin_order', # arrayref: name, name, name...
75             'plugin_setters', # hashref: { plugin_name => { key_name => coderef } }
76             'extra_config', # hashref: extra config options; name => values
77             'spawn_lock', # bool: if true, we're currently in spawn_backends
78             'extra_headers', # { insert => [ [ header, value ], ... ], remove => [ header, header, ... ],
79             # set => [ [ header, value ], ... ] }; used in header management interface
80             'generation', # int; generation count so we can slough off backends from old pools
81             'backend_no_spawn', # { "ip:port" => 1 }; if on, spawn_backends will ignore this ip:port combo
82             'buffer_backend_connect', # 0 for of, else, number of bytes to buffer before we ask for a backend
83             'selector', # CODE ref, or undef, for role 'selector' services
84             'default_service', # Perlbal::Service; name of a service a selector should default to
85             'buffer_uploads', # bool; enable/disable the buffered uploads to disk system
86             'buffer_uploads_path', # string; path to store buffered upload files
87             'buffer_upload_threshold_time', # int; buffer uploads estimated to take longer than this
88             'buffer_upload_threshold_size', # int; buffer uploads greater than this size (in bytes)
89             'buffer_upload_threshold_rate', # int; buffer uploads uploading at less than this rate (in bytes/sec)
90              
91             'upload_status_listeners', # string: comma separated list of ip:port of UDP upload status receivers
92             'upload_status_listeners_sockaddr', # arrayref of sockaddrs (packed ip/port)
93              
94             'enable_ssl', # bool: whether this service speaks SSL to the client
95             'ssl_key_file', # file: path to key pem file
96             'ssl_cert_file', # file: path to key pem file
97             'ssl_cipher_list', # OpenSSL cipher list string
98             'ssl_ca_path', # directory: path to certificates
99             'ssl_verify_mode', # int: verification mode, see IO::Socket::SSL documentation
100              
101             'enable_error_retries', # bool: whether we should retry requests after errors
102             'error_retry_schedule', # string of comma-separated seconds (full or partial) to delay between retries
103             'latency', # int: milliseconds of latency to add to request
104             'server_tokens', # bool: whether to provide a "Server" header
105              
106             # stats:
107             '_stat_requests', # total requests to this service
108             '_stat_cache_hits', # total requests to this service that were served via the reproxy-url cache
109 22     22   133 );
  22         46  
110              
111             # hash; 'role' => coderef to instantiate a client for this role
112             our %PluginRoles;
113              
114             # used by set_defaults
115             our $defaults = {};
116              
117             our $tunables = {
118              
119             'role' => {
120             des => "What type of service. One of 'reverse_proxy' for a service that load balances to a pool of backend webserver nodes, 'web_server' for a typical webserver', 'management' for a Perlbal management interface (speaks both command-line or HTTP, auto-detected), or 'selector', for a virtual service that maps onto other services.",
121             required => 1,
122              
123             check_type => sub {
124             my ($self, $val, $errref) = @_;
125             return 0 unless $val;
126             return 1 if $val =~ /^(?:reverse_proxy|web_server|management|selector|upload_tracker)$/;
127             return 1 if $PluginRoles{$val};
128             $$errref = "Role not valid for service $self->{name}";
129             return 0;
130             },
131             check_role => '*',
132             setter => sub {
133             my ($self, $val, $set, $mc) = @_;
134             my $rv = $set->();
135             $self->init; # now that service role is set
136             return $rv;
137             },
138             },
139              
140             'listen' => {
141             check_role => "*",
142             des => "The ip:port to listen on. For a service to work, you must either make it listen, or make another selector service map to a non-listening service.",
143             check_type => ["regexp", qr/^(\d+\.\d+\.\d+\.\d+:)?\d+$/,
144             "Listen argument must be ip:port or port. " .
145             "e.g. 192.168.0.1:80 or 81"],
146             setter => sub {
147             my ($self, $val, $set, $mc) = @_;
148              
149             # close/reopen listening socket
150             if ($val ne ($self->{listen} || "") && $self->{enabled}) {
151             $self->disable(undef, "force");
152             $self->{listen} = $val;
153             $self->enable(undef);
154             }
155              
156             return $set->();
157             },
158             },
159              
160             'backend_persist_cache' => {
161             des => "The number of backend connections to keep alive on reserve while there are no clients.",
162             check_type => "int",
163             default => 2,
164             check_role => "reverse_proxy",
165             },
166              
167             'persist_client' => {
168             des => "Whether to enable HTTP keep-alives to the end user.",
169             default => 0,
170             check_type => "bool",
171             check_role => "*",
172             },
173              
174             'persist_backend' => {
175             des => "Whether to enable HTTP keep-alives to the backend webnodes. (Off by default, but highly recommended if Perlbal will be the only client to your backends. If not, beware that Perlbal will hog the connections, starving other clients.)",
176             default => 0,
177             check_type => "bool",
178             check_role => "reverse_proxy",
179             },
180              
181             'verify_backend' => {
182             des => "Whether Perlbal should send a quick OPTIONS request to the backends before sending an actual client request to them. If your backend is Apache or some other process-based webserver, this is HIGHLY recommended. All too often a loaded backend box will reply to new TCP connections, but it's the kernel's TCP stack Perlbal is talking to, not an actual Apache process yet. Using this option reduces end-user latency a ton on loaded sites.",
183             default => 0,
184             check_type => "bool",
185             check_role => "reverse_proxy",
186             },
187            
188             'verify_backend_path' => {
189             des => "What path the OPTIONS request sent by verify_backend should use. Default is '*'.",
190             default => '*',
191             check_role => "reverse_proxy",
192             },
193              
194             'max_backend_uses' => {
195             check_role => "reverse_proxy",
196             des => "The max number of requests to be made on a single persistent backend connection before releasing the connection. The default value of 0 means no limit, and the connection will only be discarded once the backend asks it to be, or when Perlbal is sufficiently idle.",
197             default => 0,
198             },
199              
200             'max_put_size' => {
201             default => 0, # no limit
202             des => "The maximum content-length that will be accepted for a PUT request, if enable_put is on. Default value of 0 means no limit.",
203             check_type => "size",
204             check_role => "web_server",
205             },
206              
207             'max_chunked_request_size' => {
208             default => 209715200, # 200 MB. (0: no limit)
209             des => "The maximum size that will be accepted for a chunked request. Default is 200MB (which is written to disk, buffered uploads must be on). A value of 0 means no limit.",
210             check_type => "size",
211             check_role => "*",
212             },
213              
214             'buffer_size' => {
215             des => "How much we'll ahead of a client we'll get while copying from a backend to a client. If a client gets behind this much, we stop reading from the backend for a bit.",
216             default => "256k",
217             check_type => "size",
218             check_role => "reverse_proxy",
219             },
220              
221             'buffer_size_reproxy_url' => {
222             des => "How much we'll get ahead of a client we'll get while copying from a reproxied URL to a client. If a client gets behind this much, we stop reading from the reproxied URL for a bit. The default is lower than the regular buffer_size (50k instead of 256k) because it's assumed that you're only reproxying to large files on event-based webservers, which are less sensitive to many open connections, whereas the 256k buffer size is good for keeping heavy process-based free of slow clients.",
223             default => "50k",
224             check_type => "size",
225             check_role => "reverse_proxy",
226             },
227              
228             'queue_relief_size' => {
229             default => 0,
230             check_type => "int",
231             check_role => "reverse_proxy",
232             },
233              
234             'queue_relief_chance' => {
235             default => 0,
236             check_type => sub {
237             my ($self, $val, $errref) = @_;
238             return 1 if $val =~ /^\d+$/ && $val >= 0 && $val <= 100;
239             $$errref = "Expecting integer value between 0 and 100, inclusive.";
240             return 0;
241             },
242             check_role => "reverse_proxy",
243             },
244              
245             'buffer_backend_connect' => {
246             des => "How much content-body (POST/PUT/etc) data we read from a client before we start sending it to a backend web node. If 'buffer_uploads' is enabled, this value is used to determine how many bytes are read before Perlbal makes a determination on whether or not to spool the upload to disk.",
247             default => '100k',
248             check_type => "size",
249             check_role => "reverse_proxy",
250             },
251              
252             'docroot' => {
253             des => "Directory root for web server.",
254              
255             check_role => "web_server",
256             val_modify => sub { my $valref = shift; $$valref =~ s!/$!!; },
257             check_type => sub {
258             my ($self, $val, $errref) = @_;
259             #FIXME: require absolute paths?
260             return 1 if $val && -d $val;
261             $$errref = "Directory not found for service $self->{name}";
262             return 0;
263             },
264             },
265              
266             'enable_put' => {
267             des => "Enable HTTP PUT requests.",
268             default => 0,
269             check_role => "web_server",
270             check_type => "bool",
271             },
272              
273             'enable_md5' => {
274             des => "Enable verification of the Content-MD5 header in HTTP PUT requests",
275             default => 1,
276             check_role => "web_server",
277             check_type => "bool",
278             },
279              
280             'enable_delete' => {
281             des => "Enable HTTP DELETE requests.",
282             default => 0,
283             check_role => "web_server",
284             check_type => "bool",
285             },
286              
287             'enable_reproxy' => {
288             des => "Enable 'reproxying' (end-user-transparent internal redirects) to either local files or other URLs. When enabled, the backend servers in the pool that this service is configured for will have access to tell this Perlbal instance to serve any local readable file, or connect to any other URL that this Perlbal can connect to. Only enable this if you trust the backend web nodes.",
289             default => 0,
290             check_role => "reverse_proxy",
291             check_type => "bool",
292             },
293              
294             'reproxy_cache_maxsize' => {
295             des => "Set the maximum number of cached reproxy results (X-REPROXY-CACHE-FOR) that may be kept in the service cache. These cached requests take up about 1.25KB of ram each (on Linux x86), but will vary with usage. Perlbal still starts with 0 in the cache and will grow over time. Be careful when adjusting this and watch your ram usage like a hawk.",
296             default => 0,
297             check_role => "reverse_proxy",
298             check_type => "int",
299             setter => sub {
300             my ($self, $val, $set, $mc) = @_;
301             if ($val) {
302             $self->{reproxy_cache} ||= Perlbal::Cache->new(maxsize => 1);
303             $self->{reproxy_cache}->set_maxsize($val);
304             } else {
305             $self->{reproxy_cache} = undef;
306             }
307             return $mc->ok;
308             },
309             },
310              
311             'upload_status_listeners' => {
312             des => "Comma separated list of hosts in form 'a.b.c.d:port' which will receive UDP upload status packets no faster than once a second per HTTP request (PUT/POST) from clients that have requested an upload status bar, which they request by appending the URL get argument ?client_up_sess=[xxxxxx] where xxxxx is 5-50 'word' characters (a-z, A-Z, 0-9, underscore).",
313             default => "",
314             check_role => "reverse_proxy",
315             check_type => sub {
316             my ($self, $val, $errref) = @_;
317             my @packed;
318             foreach my $ipa (grep { $_ } split(/\s*,\s*/, $val)) {
319             unless ($ipa =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/) {
320             $$errref = "Invalid UDP endpoint: \"$ipa\". Must be of form a.b.c.d:port";
321             return 0;
322             }
323             push @packed, scalar Socket::sockaddr_in($2, Socket::inet_aton($1));
324             }
325             $self->{upload_status_listeners_sockaddr} = \@packed;
326             return 1;
327             },
328             },
329              
330             'min_put_directory' => {
331             des => "If PUT requests are enabled, require this many levels of directories to already exist. If not, fail.",
332             default => 0, # no limit
333             check_role => "web_server",
334             check_type => "int",
335             },
336              
337             'dirindexing' => {
338             des => "Show directory indexes when an HTTP request is for a directory. Warning: this is not an async operation, so will slow down Perlbal on heavily loaded sites.",
339             default => 0,
340             check_role => "web_server",
341             check_type => "bool",
342             },
343              
344             'enable_concatenate_get' => {
345             des => "Enable Perlbal's multiple-files-in-one-request mode, where a client have use a comma-separated list of files to return, always in text/plain. Useful for web apps which have dozens/hundreds of tiny css/js files, and don't trust browsers/etc to do pipelining. Decreases overall round-trip latency a bunch, but requires app to be modified to support it. See t/17-concat.t test for details.",
346             default => 0,
347             check_role => "web_server",
348             check_type => "bool",
349             },
350              
351             'connect_ahead' => {
352             des => "How many extra backend connections we keep alive in addition to the current ones, in anticipation of new client connections.",
353             default => 0,
354             check_type => "int",
355             check_role => "reverse_proxy",
356             setter => sub {
357             my ($self, $val, $set, $mc) = @_;
358             my $rv = $set->();
359             $self->spawn_backends if $self->{enabled};
360             return $rv;
361             },
362             },
363              
364             'always_trusted' => {
365             des => "Whether to trust all incoming requests' X-Forwarded-For and related headers. Set to true only if you know that all incoming requests from your own proxy servers that clean/set those headers.",
366             default => 0,
367             check_type => "bool",
368             check_role => "*",
369             },
370              
371             'blind_proxy' => {
372             des => "Flag to disable any modification of X-Forwarded-For, X-Host, and X-Forwarded-Host headers.",
373             default => 0,
374             check_type => "bool",
375             check_role => "reverse_proxy",
376             },
377              
378             'high_priority_cookie' => {
379             des => "The cookie name to inspect to determine if the client goes onto the high-priority queue.",
380             check_role => "reverse_proxy",
381             },
382              
383             'high_priority_cookie_contents' => {
384             des => "A string that the high_priority_cookie must contain to go onto the high-priority queue.",
385             check_role => "reverse_proxy",
386             },
387              
388             'trusted_upstream_proxies' => {
389             des => "A comma separated list of Net::Netmask filters (e.g. 10.0.0.0/24, see Net::Netmask) that determines whether upstream clients are trusted or not, where trusted means their X-Forwarded-For/etc headers are not munged.",
390             check_role => "*",
391             check_type => sub {
392             my ($self, $val, $errref) = @_;
393             unless (my $loaded = eval { require Net::Netmask; 1; }) {
394             $$errref = "Net::Netmask not installed";
395             return 0;
396             }
397              
398             my @val = split /\s*,\s*/, $val;
399             my @trusted_upstreams = ();
400              
401             for my $ip (@val) {
402             my $net = Net::Netmask->new2($ip);
403             unless ($net) {
404             $$errref = "Error defining trusted upstream proxies: " . Net::Netmask::errstr();
405             return 0;
406             }
407             push @trusted_upstreams, $net;
408             }
409              
410             unless (@trusted_upstreams) {
411             $$errref = "Error defining trusted upstream proxies: None found";
412             return 0;
413             }
414             $self->{trusted_upstream_proxies} = \@trusted_upstreams;
415             },
416             setter => sub {
417             my ($self, $val, $set, $mc) = @_;
418             # Do nothing here, we don't want the default setter because we've
419             # already set the value in the type_check step.
420             return $mc->ok;
421             },
422             },
423              
424             'index_files' => {
425             check_role => "web_server",
426             default => "index.html",
427             des => "Comma-separated list of filenames to load when a user visits a directory URL, listed in order of preference.",
428             setter => sub {
429             my ($self, $val, $set, $mc) = @_;
430             $self->{index_files} = [ split(/[\s,]+/, $val) ];
431             return $mc->ok;
432             },
433             dumper => sub {
434             my ($self, $val) = @_;
435             return unless defined $val;
436             return join(', ', @$val);
437             },
438             },
439              
440             'default_service' => {
441             des => "Name of previously-created service to default requests that aren't matched by a selector plugin to.",
442             check_role => "selector",
443             check_type => sub {
444             my ($self, $val, $errref) = @_;
445              
446             my $svc = Perlbal->service($val);
447             unless ($svc) {
448             $$errref = "Service '$svc' not found";
449             return 0;
450             }
451              
452             $self->{default_service} = $svc;
453             return 1;
454             },
455             setter => sub {
456             # override default so we don't set it to the text
457             return $_[3]->ok;
458             },
459             },
460              
461             'pool' => {
462             des => "Name of previously-created pool object containing the backend nodes that this reverse proxy sends requests to.",
463             check_role => "reverse_proxy",
464             check_type => sub {
465             my ($self, $val, $errref) = @_;
466             my $pl = Perlbal->pool($val);
467             unless ($pl) {
468             $$errref = "Pool '$val' not found";
469             return 0;
470             }
471             $self->{pool}->decrement_use_count if $self->{pool};
472             $self->{pool} = $pl;
473             $self->{pool}->increment_use_count;
474             $self->{generation}++;
475             return 1;
476             },
477             setter => sub {
478             my ($self, $val, $set, $mc) = @_;
479             # override the default, which is to set "pool" to the
480             # stringified name of the pool, but we already set it in
481             # the type-checking phase. instead, we do nothing here.
482             return $mc->ok;
483             },
484             dumper => sub {
485             my ($self, $val) = @_;
486             return $val->name;
487             }
488             },
489              
490             'server_process' => {
491             des => "Executable which will be the HTTP server on stdin/stdout. (ALPHA, EXPERIMENTAL!)",
492             check_role => "reverse_proxy",
493             check_type => sub {
494             my ($self, $val, $errref) = @_;
495             #FIXME: require absolute paths?
496             return 1 if $val && -x $val;
497             $$errref = "Server process ($val) not executable.";
498             return 0;
499             },
500             },
501              
502             'persist_client_timeout' => {
503             des => "Set both the persist_client_idle_timeout and idle_timeout (deprecated)",
504             check_type => "int",
505             check_role => "*",
506             setter => sub {
507             my ($self, $val, $set, $mc) = @_;
508             $self->{persist_client_idle_timeout} = $val;
509             $self->{idle_timeout} = $val;
510             return $mc->ok;
511             },
512             dump_ignore => 1,
513             },
514              
515             'persist_client_idle_timeout' => {
516             des => "Timeout in seconds for HTTP keep-alives to the end user (default is 30)",
517             check_type => "int",
518             default => 30,
519             check_role => "*",
520             },
521              
522             'idle_timeout' => {
523             des => "Timeout in seconds for idle connections to the end user (default is 30)",
524             check_type => "int",
525             default => 30,
526             check_role => "*",
527             },
528              
529             'buffer_uploads_path' => {
530             des => "Directory root for storing files used to buffer uploads.",
531              
532             check_role => "reverse_proxy",
533             val_modify => sub { my $valref = shift; $$valref =~ s!/$!!; },
534             check_type => sub {
535             my ($self, $val, $errref) = @_;
536             #FIXME: require absolute paths?
537             return 1 if $val && -d $val;
538             $$errref = "Directory ($val) not found for service $self->{name} (buffer_uploads_path)";
539             return 0;
540             },
541             },
542              
543             'buffer_uploads' => {
544             des => "Used to enable or disable the buffer uploads to disk system. If enabled, 'buffer_backend_connect' bytes worth of the upload will be stored in memory. At that point, the buffer upload thresholds will be checked to see if we should just send this upload to the backend, or if we should spool it to disk.",
545             default => 0,
546             check_role => "reverse_proxy",
547             check_type => "bool",
548             },
549              
550             'buffer_upload_threshold_time' => {
551             des => "If an upload is estimated to take more than this number of seconds, it will be buffered to disk. Set to 0 to not check estimated time.",
552             default => 5,
553             check_role => "reverse_proxy",
554             check_type => "int",
555             },
556              
557             'buffer_upload_threshold_size' => {
558             des => "If an upload is larger than this size in bytes, it will be buffered to disk. Set to 0 to not check size.",
559             default => '250k',
560             check_role => "reverse_proxy",
561             check_type => "size",
562             },
563              
564             'buffer_upload_threshold_rate' => {
565             des => "If an upload is coming in at a rate less than this value in bytes per second, it will be buffered to disk. Set to 0 to not check rate.",
566             default => 0,
567             check_role => "reverse_proxy",
568             check_type => "int",
569             },
570              
571             'latency' => {
572             des => "Forced latency (in milliseconds) to add to request.",
573             default => 0,
574             check_role => "selector",
575             check_type => "int",
576             },
577              
578             'enable_ssl' => {
579             des => "Enable SSL to the client.",
580             default => 0,
581             check_type => "bool",
582             check_role => "*",
583             },
584              
585             'ssl_key_file' => {
586             des => "Path to private key PEM file for SSL.",
587             default => "certs/server-key.pem",
588             check_type => "file_or_none",
589             check_role => "*",
590             },
591              
592             'ssl_cert_file' => {
593             des => "Path to certificate PEM file for SSL.",
594             default => "certs/server-cert.pem",
595             check_type => "file_or_none",
596             check_role => "*",
597             },
598              
599             'ssl_cipher_list' => {
600             des => "OpenSSL-style cipher list.",
601             default => "ALL:!LOW:!EXP",
602             check_role => "*",
603             },
604              
605             'ssl_ca_path' => {
606             des => 'Path to directory containing certificates for SSL.',
607             default => undef,
608             check_type => "directory_or_none",
609             check_role => "*",
610             },
611              
612             'ssl_verify_mode' => {
613             des => 'SSL verification mode',
614             default => 0,
615             check_type => "int",
616             check_role => "*",
617             },
618              
619             'enable_error_retries' => {
620             des => 'Whether Perlbal should transparently retry requests to backends if a backend returns a 500 server error.',
621             default => 0,
622             check_type => "bool",
623             check_role => "reverse_proxy",
624             },
625              
626             'error_retry_schedule' => {
627             des => 'String of comma-separated seconds (full or partial) to delay between retries. For example "0,2" would mean do at most two retries, the first zero seconds after the first failure, and the second 2 seconds after the 2nd failure. You probably don\'t need to modify the default value',
628             default => '0,.25,.50,1,1,1,1,1',
629             check_role => "reverse_proxy",
630             },
631              
632             'client_sndbuf_size' => {
633             des => "How large to set the client's socket SNDBUF.",
634             default => 0,
635             check_type => "size",
636             check_role => '*',
637             },
638              
639             'server_tokens' => {
640             des => 'Whether to provide a "Server" header.',
641             check_role => '*',
642             check_type => 'bool',
643             default => 1,
644             },
645              
646             };
647 0     0 0 0 sub autodoc_get_tunables { return $tunables; }
648              
649             sub new {
650 44     44 0 1877 my Perlbal::Service $self = shift;
651 44 50       246 $self = fields::new($self) unless ref $self;
652              
653 44         20347 my ($name) = @_;
654              
655 44   100     165 $name ||= '';
656 44         127 $self->{name} = $name;
657 44         147 $self->{enabled} = 0;
658 44         115 $self->{extra_config} = {};
659              
660 44         118 $self->{backend_no_spawn} = {};
661 44         112 $self->{generation} = 0;
662              
663 44         97 $self->{hooks} = {};
664 44         111 $self->{plugins} = {};
665 44         112 $self->{plugin_order} = [];
666              
667             # track pending connects to backend
668 44         105 $self->{pending_connects} = {};
669 44         95 $self->{pending_connect_count} = 0;
670 44         306 $self->{bored_backends} = [];
671              
672             # waiting clients
673 44         198 $self->{waiting_clients} = [];
674 44         104 $self->{waiting_clients_highpri} = [];
675 44         100 $self->{waiting_clients_lowpri} = [];
676 44         97 $self->{waiting_client_count} = 0;
677 44         198 $self->{waiting_client_map} = {};
678              
679             # buffered upload setup
680 44         125 $self->{buffer_uploads_path} = undef;
681              
682             # don't have an object for this yet
683 44         89 $self->{trusted_upstream_proxies} = undef;
684              
685             # bare data structure for extra header info
686 44         230 $self->{extra_headers} = { remove => [], insert => [] };
687              
688             # things to watch...
689 44         171 foreach my $v (qw(pending_connects bored_backends waiting_clients
690             waiting_clients_highpri backend_no_spawn
691             waiting_client_map
692             )) {
693 264 50       708 die "Field '$v' not set" unless $self->{$v};
694 264         1087 Perlbal::track_var("svc-$name-$v", $self->{$v});
695             }
696              
697 44         214 return $self;
698             }
699              
700             # handy instance method to run some manage commands in the context of this service,
701             # without needing to worry about its name.
702             # This is intended as an internal API thing, so any output that would have been
703             # generated is just eaten.
704             sub run_manage_commands {
705 0     0 0 0 my ($self, $cmd_block) = @_;
706              
707 0         0 my $ctx = Perlbal::CommandContext->new;
708 0         0 $ctx->{last_created} = $self->name;
709 0         0 return Perlbal::run_manage_commands($cmd_block, undef, $ctx);
710             }
711              
712             # here's an alternative version of the above that runs a single command
713             sub run_manage_command {
714 0     0 0 0 my ($self, $cmd) = @_;
715              
716 0         0 my $ctx = Perlbal::CommandContext->new;
717 0         0 $ctx->{last_created} = $self->name;
718 0         0 return Perlbal::run_manage_command($cmd, undef, $ctx);
719             }
720              
721             sub dumpconfig {
722 0     0 0 0 my $self = shift;
723              
724 0         0 my @return;
725              
726 0         0 my %my_tunables = %$tunables;
727              
728             my $dump = sub {
729 0     0   0 my $setting = shift;
730 0         0 };
731              
732 0         0 foreach my $skip (qw(role listen pool)) {
733 0         0 delete $my_tunables{$skip};
734             }
735              
736 0         0 my $role = $self->{role};
737              
738 0         0 foreach my $setting ("role", "listen", "pool", sort keys %my_tunables) {
739 0         0 my $attrs = $tunables->{$setting};
740              
741 0 0       0 next if $attrs->{dump_ignore};
742              
743 0 0       0 my $value = $attrs->{_plugin_inserted} ? $self->{extra_config}->{$setting} : $self->{$setting};
744              
745 0         0 my $check_role = $attrs->{check_role};
746 0         0 my $check_type = $attrs->{check_type};
747 0         0 my $default = $attrs->{default};
748 0         0 my $required = $attrs->{required};
749              
750 0 0 0     0 next if ($check_role && $check_role ne '*' && $check_role ne $role);
      0        
751              
752 0 0 0     0 if ($check_type && $check_type eq 'size') {
753 0 0       0 $default = $1 if $default =~ /^(\d+)b$/i;
754 0 0       0 $default = $1 * 1024 if $default =~ /^(\d+)k$/i;
755 0 0       0 $default = $1 * 1024 * 1024 if $default =~ /^(\d+)m$/i;
756             }
757              
758 0 0       0 if (!$required) {
759 0 0       0 next unless defined $value;
760 0 0 0     0 next if (defined $default && $value eq $default);
761             }
762              
763 0 0       0 if (my $dumper = $attrs->{dumper}) {
764 0         0 $value = $dumper->($self, $value);
765             }
766              
767 0 0 0     0 if ($check_type && $check_type eq 'bool') {
768 0 0       0 $value = 'on' if $value;
769             }
770              
771 0         0 push @return, "SET $setting = $value";
772             }
773              
774 0         0 my $plugins = $self->{plugins};
775              
776 0         0 foreach my $plugin (keys %$plugins) {
777 0         0 local $@;
778              
779 0         0 my $class = "Perlbal::Plugin::$plugin";
780 0         0 my $cv = $class->can('dumpconfig');
781              
782 0 0       0 if ($cv) {
783 0         0 eval { push @return, $class->dumpconfig($self) };
  0         0  
784 0 0       0 if ($@) {
785 0         0 push @return, "# Plugin '$plugin' threw an exception while being dumped.";
786             }
787             } else {
788 0         0 push @return, "# Plugin '$plugin' isn't capable of dumping config.";
789             }
790             }
791              
792 0         0 return @return;
793             }
794              
795             # called once a role has been set
796             sub init {
797 43     43 0 96 my Perlbal::Service $self = shift;
798 43 50       223 die "init called when no role" unless $self->{role};
799              
800             # set all the defaults
801 43         664 for my $param (keys %$tunables) {
802 2326         3855 my $tun = $tunables->{$param};
803 2326 100 100     11667 next unless $tun->{check_role} eq "*" || $tun->{check_role} eq $self->{role};
804              
805 1092 50       3730 if (exists $defaults->{$param}) {
    100          
806 0         0 $self->set($param, $defaults->{$param});
807             } elsif (exists $tun->{default}) {
808 858         2513 $self->set($param, $tun->{default});
809             }
810             }
811             }
812              
813             # Service default setter
814             sub set_defaults {
815 0     0 0 0 my ($mc, %args) = @_;
816 0         0 foreach my $key (keys %args) {
817 0         0 $defaults->{$key} = $args{$key};
818             }
819 0         0 return $mc->ok;
820             }
821              
822             # Service
823             sub set {
824 1084     1084 0 2007 my Perlbal::Service $self = shift;
825 1084         1784 my ($key, $val, $mc) = @_;
826              
827             # if you don't provide an $mc, that better mean you're damn sure it
828             # won't crash. (end-users never go this route)
829 1084   66     4519 $mc ||= Perlbal::ManageCommand->loud_crasher;
830              
831 1084     1039   4739 my $set = sub { $self->{$key} = $val; return $mc->ok; };
  1039         2517  
  1039         4192  
832              
833             my $pool_set = sub {
834             # if we don't have a pool, automatically create one named $NAME_pool
835 0 0   0   0 unless ($self->{pool}) {
836             # die if necessary
837 0 0       0 die "ERROR: Attempt to vivify pool $self->{name}_pool but one or more pools\n" .
838             " have already been created manually. Please set $key on a\n" .
839             " previously created pool.\n" unless $Perlbal::vivify_pools;
840              
841             # create the pool and ensure that vivify stays on
842 0         0 Perlbal::run_manage_command("CREATE POOL $self->{name}_pool", $mc->out);
843 0         0 Perlbal::run_manage_command("SET $self->{name}.pool = $self->{name}_pool");
844 0         0 $Perlbal::vivify_pools = 1;
845             }
846              
847             # now we actually do the set
848 0 0       0 warn "WARNING: '$key' set on service $self->{name} on auto-vivified pool.\n" .
849             " This behavior is obsolete. This value should be set on a\n" .
850             " pool object and not on a service.\n" if $Perlbal::vivify_pools;
851 0 0       0 return $mc->err("No pool defined for service") unless $self->{pool};
852 0         0 return $self->{pool}->set($key, $val, $mc);
853 1084         5181 };
854              
855             # this is now handled by Perlbal::Pool, so we pass this set command on
856             # through in case people try to use it on us like the old method.
857 1084 50 33     5379 return $pool_set->()
858             if $key eq 'nodefile' ||
859             $key eq 'balance_method';
860              
861 1084 100       3098 if (my $tun = $tunables->{$key}) {
862 1078 50       2891 if (my $req_role = $tun->{check_role}) {
863 1078 50 100     5683 return $mc->err("The '$key' option can only be set on a '$req_role' service")
      66        
864             unless ($self->{role}||"") eq $req_role || $req_role eq "*";
865             }
866              
867 1078 100       3814 if (my $req_type = $tun->{check_type}) {
868 993 50 66     9985 if (ref $req_type eq "ARRAY" && $req_type->[0] eq "enum") {
    100 66        
    100          
    100          
    100          
    100          
    50          
    100          
    50          
869 0         0 return $mc->err("Value of '$key' must be one of: " . join(", ", @{$req_type->[1]}))
  0         0  
870 0 0       0 unless grep { $val eq $_ } @{$req_type->[1]};
  0         0  
871             } elsif (ref $req_type eq "ARRAY" && $req_type->[0] eq "regexp") {
872 40         85 my $re = $req_type->[1];
873 40         280 my $emsg = $req_type->[2];
874 40 50       554 return $mc->err($emsg) unless $val =~ /$re/;
875             } elsif (ref $req_type eq "CODE") {
876 85         151 my $emsg = "";
877 85 100       339 return $mc->err($emsg) unless $req_type->($self, $val, \$emsg);
878             } elsif ($req_type eq "bool") {
879 372         793 $val = _bool($val);
880 372 50       875 return $mc->err("Expecting boolean value for parameter '$key'")
881             unless defined $val;
882             } elsif ($req_type eq "int") {
883 221 50       1214 return $mc->err("Expecting integer value for parameter '$key'")
884             unless $val =~ /^\d+$/;
885             } elsif ($req_type eq "size") {
886 146 50       889 $val = $1 if $val =~ /^(\d+)b$/i;
887 146 100       934 $val = $1 * 1024 if $val =~ /^(\d+)k$/i;
888 146 50       691 $val = $1 * 1024 * 1024 if $val =~ /^(\d+)m$/i;
889 146 50       833 return $mc->err("Expecting size unit value for parameter '$key' in bytes, or suffixed with 'K' or 'M'")
890             unless $val =~ /^\d+$/;
891             } elsif ($req_type eq "file") {
892 0 0       0 return $mc->err("File '$val' not found for '$key'") unless -f $val;
893             } elsif ($req_type eq "file_or_none") {
894 86 50 33     1830 return $mc->err("File '$val' not found for '$key'") unless -f $val || $val eq $tun->{default};
895             } elsif ($req_type eq "directory_or_none") {
896 43 50 33     244 return $mc->err("Directory '$val' not found for '$key'") unless !defined $val || -d $val;
897             } else {
898 0         0 die "Unknown check_type: $req_type\n";
899             }
900             }
901              
902 1077 100       3164 if ($tun->{_plugin_inserted}) {
903             # plugins that add tunables need to be stored in the extra_config hash due to the main object
904             # using fields. this passthrough is done so the config files don't need to specify this.
905             $set = sub {
906 4     4   9 $self->{extra_config}->{$key} = $val;
907 4         10 return $mc->ok;
908 4         10 };
909             }
910              
911 1077         1509 my $setter = $tun->{setter};
912              
913 1077 100       2092 if (ref $setter eq "CODE") {
914 134         447 return $setter->($self, $val, $set, $mc);
915             } else {
916 943         1909 return $set->();
917             }
918             }
919              
920 6 50       21 if ($key eq 'plugins') {
921             # unload existing plugins
922 6         12 foreach my $plugin (keys %{$self->{plugins}}) {
  6         25  
923 0         0 eval "Perlbal::Plugin::$plugin->unregister(\$self);";
924 0 0       0 return $mc->err($@) if $@;
925             }
926              
927             # clear out loaded plugins and hooks
928 6         20 $self->{hooks} = {};
929 6         15 $self->{plugins} = {};
930 6         16 $self->{plugin_order} = [];
931              
932             # load some plugins
933 6         32 foreach my $plugin (split /[\s,]+/, $val) {
934 6 50       18 next if $plugin eq 'none';
935              
936 6         26 my $fn = Perlbal::plugin_case($plugin);
937              
938 6 50       27 next if $self->{plugins}->{$fn};
939 6 50       23 unless ($Perlbal::plugins{$fn}) {
940 0         0 $mc->err("Plugin $fn not loaded; not registered for $self->{name}.");
941 0         0 next;
942             }
943              
944             # now register it
945 6         514 eval "Perlbal::Plugin::$fn->register(\$self);";
946 6 50       30 return $mc->err($@) if $@;
947 6         18 $self->{plugins}->{$fn} = 1;
948 6         11 push @{$self->{plugin_order}}, $fn;
  6         24  
949             }
950 6         27 return $mc->ok;
951             }
952              
953 0 0       0 if ($key =~ /^extra\.(.+)$/) {
954             # set some extra configuration data data
955 0         0 $self->{extra_config}->{$1} = $val;
956 0         0 return $mc->ok;
957             }
958              
959             # see if it happens to be a plugin set command?
960 0 0       0 if ($key =~ /^(.+)\.(.+)$/) {
961 0 0       0 if (my $coderef = $self->{plugin_setters}->{$1}->{$2}) {
962 0         0 return $coderef->($mc->out, $2, $val);
963             }
964             }
965              
966 0         0 return $mc->err("Unknown service parameter '$key'");
967             }
968              
969             {
970             # should use sate, but could be a problem for old perl version
971             # benchmark test, says that this function is x5 times faster than the previous one
972             my %h_on = map { $_, 1 } qw/1 true on yes/;
973             my %h_off = map { $_, 1 } qw/0 false off no/;
974              
975             # create one static method to check boolean value, rather than generating each time a dynamic method
976             # use static list rather than regexp to speedup the reading process
977             sub _bool {
978 100373     100373   2404547 my $val = shift;
979              
980 100373 50       191844 return unless defined $val;
981              
982 100373         174345 $val = lc($val);
983 100373 100       211753 return 1 if defined $h_on{$val};
984 100264 100       177852 return 0 if defined $h_off{$val};
985              
986 100001         2069329 return undef;
987             }
988             }
989              
990             # CLASS METHOD -
991             # used by plugins that want to add tunables so that the config file
992             # can have more options for service settings
993             sub add_tunable {
994 2     2 0 4 my ($name, $hashref) = @_;
995 2 50 33     24 return 0 unless $name && $hashref && ref $hashref eq 'HASH';
      33        
996 2 50       9 return 0 if $tunables->{$name};
997 2         5 $hashref->{_plugin_inserted} = 1; # mark that a plugin did this
998 2         4 $tunables->{$name} = $hashref;
999 2         27 return 1;
1000             }
1001              
1002             # CLASS METHOD -
1003             # remove a defined tunable, but only if a plugin is what created it
1004             sub remove_tunable {
1005 0     0 0 0 my $name = shift;
1006 0 0       0 my $tun = $tunables->{$name} or return 0;
1007 0 0       0 return 0 unless $tun->{_plugin_inserted};
1008 0         0 delete $tunables->{$name};
1009 0         0 return 1;
1010             }
1011              
1012             # CLASS METHOD -
1013             # used by plugins to define a new role that services can take on
1014             sub add_role {
1015 0     0 0 0 my ($role, $creator) = @_;
1016 0 0 0     0 return 0 unless $role && $creator && ref $creator eq 'CODE';
      0        
1017 0 0       0 return 0 if $PluginRoles{$role};
1018 0         0 $PluginRoles{$role} = $creator;
1019 0         0 return 1;
1020             }
1021              
1022             # CLASS METHOD -
1023             # remove a defined plugin role
1024             sub remove_role {
1025 0 0   0 0 0 return 0 unless delete $PluginRoles{$_[0]};
1026 0         0 return 1;
1027             }
1028              
1029             # CLASS METHOD -
1030             # returns a defined role creator, if it exists. (undef if it does not)
1031             sub get_role_creator {
1032 0     0 0 0 return $PluginRoles{$_[0]};
1033             }
1034              
1035             # run the hooks in a list one by one until one hook returns a true
1036             # value. returns 1 or 0 depending on if any hooks handled the
1037             # request.
1038             sub run_hook {
1039 1664     1664 0 3517 my Perlbal::Service $self = shift;
1040 1664         5797 my $hook = shift;
1041 1664 100       10435 if (defined (my $ref = $self->{hooks}->{$hook})) {
1042             # call all the hooks until one returns true
1043 2         6 foreach my $hookref (@$ref) {
1044 2         14 my $rval = $hookref->[1]->(@_);
1045 2 100       13 return 1 if $rval;
1046             }
1047             }
1048 1663         11202 return 0;
1049             }
1050              
1051             # run a bunch of hooks in this service, always returns undef.
1052             sub run_hooks {
1053 146     146 0 332 my Perlbal::Service $self = shift;
1054 146         271 my $hook = shift;
1055 146 50       823 if (defined (my $ref = $self->{hooks}->{$hook})) {
1056             # call all the hooks
1057 0         0 $_->[1]->(@_) foreach @$ref;
1058             }
1059 146         479 return undef;
1060             }
1061              
1062             # define a hook for this service
1063             sub register_hook {
1064 2     2 0 6 my Perlbal::Service $self = shift;
1065 2         4 my ($pclass, $hook, $ref) = @_;
1066 2   50     5 push @{$self->{hooks}->{$hook} ||= []}, [ $pclass, $ref ];
  2         19  
1067 2         16 return 1;
1068             }
1069              
1070             # remove hooks we have defined
1071             sub unregister_hook {
1072 0     0 0 0 my Perlbal::Service $self = shift;
1073 0         0 my ($pclass, $hook) = @_;
1074 0 0       0 if (defined (my $refs = $self->{hooks}->{$hook})) {
1075 0         0 my @new;
1076 0         0 foreach my $ref (@$refs) {
1077             # fill @new with hooks that DON'T match
1078 0 0       0 push @new, $ref
1079             unless $ref->[0] eq $pclass;
1080             }
1081 0         0 $self->{hooks}->{$hook} = \@new;
1082 0         0 return 1;
1083             }
1084 0         0 return undef;
1085             }
1086              
1087             # remove all hooks of a certain class
1088             sub unregister_hooks {
1089 0     0 0 0 my Perlbal::Service $self = shift;
1090 0         0 foreach my $hook (keys %{$self->{hooks}}) {
  0         0  
1091             # call unregister_hook with this hook name
1092 0         0 $self->unregister_hook($_[0], $hook);
1093             }
1094             }
1095              
1096             # register a value setter for plugin configuration
1097             sub register_setter {
1098 0     0 0 0 my Perlbal::Service $self = shift;
1099 0         0 my ($pclass, $key, $coderef) = @_;
1100 0 0 0     0 return unless $pclass && $key && $coderef;
      0        
1101 0         0 $self->{plugin_setters}->{lc $pclass}->{lc $key} = $coderef;
1102             }
1103              
1104             # remove a setter
1105             sub unregister_setter {
1106 0     0 0 0 my Perlbal::Service $self = shift;
1107 0         0 my ($pclass, $key) = @_;
1108 0 0 0     0 return unless $pclass && $key;
1109 0         0 delete $self->{plugin_setters}->{lc $pclass}->{lc $key};
1110             }
1111              
1112             # remove a bunch of setters
1113             sub unregister_setters {
1114 0     0 0 0 my Perlbal::Service $self = shift;
1115 0         0 my $pclass = shift;
1116 0 0       0 return unless $pclass;
1117 0         0 delete $self->{plugin_setters}->{lc $pclass};
1118             }
1119              
1120             # take a backend we've created and mark it as pending if we do not
1121             # have another pending backend connection in this slot
1122             sub add_pending_connect {
1123 20     20 0 38 my Perlbal::Service $self = shift;
1124 20         47 my Perlbal::BackendHTTP $be = shift;
1125              
1126             # error if we already have a pending connection for this ipport
1127 20 50       126 if (defined $self->{pending_connects}{$be->{ipport}}) {
1128 0         0 Perlbal::log('warning', "Warning: attempting to spawn backend connection that already existed.");
1129              
1130             # now dump a backtrace so we know how we got here
1131 0         0 my $depth = 0;
1132 0         0 while (my ($package, $filename, $line, $subroutine) = caller($depth++)) {
1133 0         0 Perlbal::log('warning', " -- [$filename:$line] $package::$subroutine");
1134             }
1135              
1136             # we're done now, just return
1137 0         0 return;
1138             }
1139              
1140             # set this connection up in the pending connection list
1141 20         67 $self->{pending_connects}{$be->{ipport}} = $be;
1142 20         139 $self->{pending_connect_count}++;
1143             }
1144              
1145             # remove a backend connection from the pending connect list if and only
1146             # if it is the actual connection contained in the list; prevent double
1147             # decrementing on accident
1148             sub clear_pending_connect {
1149 36     36 0 74 my Perlbal::Service $self = shift;
1150 36         75 my Perlbal::BackendHTTP $be = shift;
1151 36 50 66     392 if (defined $self->{pending_connects}{$be->{ipport}} && defined $be &&
      66        
1152             $self->{pending_connects}{$be->{ipport}} == $be) {
1153 20         53 $self->{pending_connects}{$be->{ipport}} = undef;
1154 20         59 $self->{pending_connect_count}--;
1155             }
1156             }
1157              
1158             # called by BackendHTTP when it's closed by any means
1159             sub note_backend_close {
1160 16     16 0 31 my Perlbal::Service $self = shift;
1161 16         33 my Perlbal::BackendHTTP $be = shift;
1162 16         67 $self->clear_pending_connect($be);
1163 16         130 $self->spawn_backends;
1164             }
1165              
1166             # called by ClientProxy when it dies.
1167             sub note_client_close {
1168 24     24 0 118 my Perlbal::Service $self;
1169             my Perlbal::ClientProxy $cp;
1170 24         134 ($self, $cp) = @_;
1171              
1172 24 50       169 if (delete $self->{waiting_client_map}{$cp->{fd}}) {
1173 0         0 $self->{waiting_client_count}--;
1174             }
1175             }
1176              
1177             sub mark_node_used {
1178 135     135 0 259 my Perlbal::Service $self = $_[0];
1179 135 50       1569 $self->{pool}->mark_node_used($_[1]) if $self->{pool};
1180             }
1181              
1182             sub get_client {
1183 139     139 0 263 my Perlbal::Service $self = shift;
1184              
1185             my $ret = sub {
1186 20     20   43 my Perlbal::ClientProxy $cp = shift;
1187 20         43 $self->{waiting_client_count}--;
1188 20         78 delete $self->{waiting_client_map}{$cp->{fd}};
1189              
1190             # before we return, start another round of connections
1191 20         73 $self->spawn_backends;
1192              
1193 20         129 return $cp;
1194 139         1094 };
1195              
1196             # determine if we should jump straight to the high priority queue or
1197             # act as pressure relief on the standard queue
1198 139         258 my $hp_first = 1;
1199 139 50 33     698 if (($self->{queue_relief_size} > 0) &&
  0         0  
1200             (scalar(@{$self->{waiting_clients}}) >= $self->{queue_relief_size})) {
1201             # if we're below the chance level, take a standard queue item
1202 0 0       0 $hp_first = 0
1203             if rand(100) < $self->{queue_relief_chance};
1204             }
1205              
1206             # find a high-priority client, or a regular one
1207 139         209 my Perlbal::ClientProxy $cp;
1208 139   33     498 while ($hp_first && ($cp = shift @{$self->{waiting_clients_highpri}})) {
  139         1359  
1209 0 0       0 next if $cp->{closed};
1210 0         0 if (Perlbal::DEBUG >= 2) {
1211             my $backlog = scalar @{$self->{waiting_clients}};
1212             print "Got from fast queue, in front of $backlog others\n";
1213             }
1214 0         0 return $ret->($cp);
1215             }
1216              
1217             # regular clients:
1218 139         266 while ($cp = shift @{$self->{waiting_clients}}) {
  139         890  
1219 20 50       100 next if $cp->{closed};
1220 20         34 print "Backend requesting client, got normal = $cp->{fd}.\n" if Perlbal::DEBUG >= 2;
1221 20         62 return $ret->($cp);
1222             }
1223              
1224             # low-priority (batch/idle) clients.
1225 119         292 while ($cp = shift @{$self->{waiting_clients_lowpri}}) {
  119         810  
1226 0 0       0 next if $cp->{closed};
1227 0         0 print "Backend requesting client, got low priority = $cp->{fd}.\n" if Perlbal::DEBUG >= 2;
1228 0         0 return $ret->($cp);
1229             }
1230              
1231 119         1095 return undef;
1232             }
1233              
1234             # given a backend, verify it's generation
1235             sub verify_generation {
1236 254     254 0 758 my Perlbal::Service $self = $_[0];
1237 254         7331 my Perlbal::BackendHTTP $be = $_[1];
1238              
1239             # fast cases: generation count matches, so we just return an 'okay!' flag
1240 254 50       2007 return 1 if $self->{generation} == $be->generation;
1241              
1242             # if our current pool knows about this ip:port, then we can still use it
1243 0 0       0 if (defined $self->{pool}->node_used($be->ipport)) {
1244             # so we know this is good, in the future we just want to hit the fast case
1245             # and continue, so let's update the generation
1246 0         0 $be->generation($self->{generation});
1247 0         0 return 1;
1248             }
1249              
1250             # if we get here, the backend should be closed
1251 0         0 $be->close('invalid_generation');
1252 0         0 return 0;
1253             }
1254              
1255             # called by backend connection after it becomes writable
1256             sub register_boredom {
1257 139     139 0 285 my Perlbal::Service $self;
1258             my Perlbal::BackendHTTP $be;
1259 139         380 ($self, $be) = @_;
1260              
1261             # note that this backend is no longer pending a connect,
1262             # if we thought it was before. but not if it's a persistent
1263             # connection asking to be re-used.
1264 139 100       770 unless ($be->{use_count}) {
1265 20         83 $self->clear_pending_connect($be);
1266             }
1267              
1268             # it is possible that this backend is part of a different pool that we're
1269             # no longer using... if that's the case, we want to close it
1270 139 50       1076 return unless $self->verify_generation($be);
1271              
1272             # now try to fetch a client for it
1273 139         563 my Perlbal::ClientProxy $cp = $self->get_client;
1274 139 100       843 if ($cp) {
1275 20 50       105 return if $be->assign_client($cp);
1276              
1277             # don't want to lose client, so we (unfortunately)
1278             # stick it at the end of the waiting queue.
1279             # fortunately, assign_client shouldn't ever fail.
1280 0         0 $self->request_backend_connection($cp);
1281             }
1282              
1283             # don't hang onto more bored, persistent connections than
1284             # has been configured for connect-ahead
1285 119 50       384 if ($be->{use_count}) {
1286 119         399 my $current_bored = scalar @{$self->{bored_backends}};
  119         380  
1287 119 50       596 if ($current_bored >= $self->{backend_persist_cache}) {
1288 0         0 $be->close('too_many_bored');
1289 0         0 return;
1290             }
1291             }
1292              
1293             # put backends which are known to be bound to processes
1294             # and not to TCP stacks at the beginning where they'll
1295             # be used first
1296 119 50       330 if ($be->{has_attention}) {
1297 119         160 unshift @{$self->{bored_backends}}, $be;
  119         803  
1298             } else {
1299 0         0 push @{$self->{bored_backends}}, $be;
  0         0  
1300             }
1301             }
1302              
1303             sub note_bad_backend_connect {
1304 0     0 0 0 my Perlbal::Service $self = shift;
1305 0         0 my Perlbal::BackendHTTP $be = shift;
1306 0         0 my $retry_time = shift();
1307              
1308             # clear this pending connection
1309 0         0 $self->clear_pending_connect($be);
1310              
1311             # mark this host as dead for a while if we need to
1312 0 0 0     0 if (defined $retry_time && $retry_time > 0) {
1313             # we don't want other spawn_backends calls to retry
1314 0         0 $self->{backend_no_spawn}->{$be->{ipport}} = 1;
1315              
1316             # and now we set a callback to ensure we're kicked at the right time
1317             Perlbal::Socket::register_callback($retry_time, sub {
1318 0     0   0 delete $self->{backend_no_spawn}->{$be->{ipport}};
1319 0         0 $self->spawn_backends;
1320 0         0 });
1321             }
1322              
1323             # FIXME: do something interesting (tell load balancer about dead host,
1324             # and fire up a new connection, if warranted)
1325              
1326             # makes a new connection, if needed
1327 0         0 $self->spawn_backends;
1328             }
1329              
1330             sub request_backend_connection { # : void
1331 135     135 0 265 my Perlbal::Service $self;
1332             my Perlbal::ClientProxy $cp;
1333 135         361 ($self, $cp) = @_;
1334              
1335 135 50 33     1216 return unless $cp && ! $cp->{closed};
1336              
1337 135         495 my $hi_pri = $cp->{high_priority}; # load values from the client proxy object
1338 135         962 my $low_pri = $cp->{low_priority}; # they are initialized as 0 during object creation, but hooks can override them
1339              
1340             # is there a defined high-priority cookie?
1341 135 50       836 if (my $cname = $self->{high_priority_cookie}) {
1342             # decide what priority class this request is in
1343 0         0 my $hd = $cp->{req_headers};
1344 0         0 my %cookie;
1345 0   0     0 foreach (split(/;\s+/, $hd->header("Cookie") || '')) {
1346 0 0       0 next unless ($_ =~ /(.*)=(.*)/);
1347 0         0 $cookie{Perlbal::Util::durl($1)} = Perlbal::Util::durl($2);
1348             }
1349 0   0     0 my $hicookie = $cookie{$cname} || "";
1350 0         0 $hi_pri = index($hicookie, $self->{high_priority_cookie_contents}) != -1;
1351             }
1352              
1353             # now, call hook to see if this should be high priority
1354 135 50       628 $hi_pri = $self->run_hook('make_high_priority', $cp)
1355             unless $hi_pri; # only if it's not already
1356              
1357             # and then, call hook to see about low priority
1358 135 50 33     1026 $low_pri = $self->run_hook('make_low_priority', $cp)
1359             unless $hi_pri || $low_pri; # only if it's not high or low already
1360              
1361 135 50       508 $cp->{high_priority} = 1 if $hi_pri;
1362 135 50       533 $cp->{low_priority} = 1 if $low_pri;
1363              
1364             # before we even consider spawning backends, let's see if we have
1365             # some bored (pre-connected) backends that'd take this client
1366 135         248 my Perlbal::BackendHTTP $be;
1367 135         318 my $now = time;
1368 135         249 while ($be = shift @{$self->{bored_backends}}) {
  135         761  
1369 115 50       597 next if $be->{closed};
1370              
1371             # now make sure that it's still in our pool, and if not, close it
1372 115 50       603 next unless $self->verify_generation($be);
1373              
1374             # don't use connect-ahead connections when we haven't
1375             # verified we have their attention
1376 115 50 33     942 if (! $be->{has_attention} && $be->{create_time} < $now - 5) {
1377 0         0 $be->close("too_old_bored");
1378 0         0 next;
1379             }
1380              
1381             # don't use keep-alive connections if we know the server's
1382             # just about to kill the connection for being idle
1383 115 50 33     908 if ($be->{disconnect_at} && $now + 2 > $be->{disconnect_at}) {
1384 0         0 $be->close("too_close_disconnect");
1385 0         0 next;
1386             }
1387              
1388             # give the backend this client
1389 115 50       2181 if ($be->assign_client($cp)) {
1390             # and make some extra bored backends, if configured as such
1391 115         737 $self->spawn_backends;
1392 115         401 return;
1393             }
1394              
1395             # assign client can end up closing the connection, so check for that
1396 0 0       0 return if $cp->{closed};
1397             }
1398              
1399 20 50       107 if ($hi_pri) {
    50          
1400 0         0 push @{$self->{waiting_clients_highpri}}, $cp;
  0         0  
1401             } elsif ($low_pri) {
1402 0         0 push @{$self->{waiting_clients_lowpri}}, $cp;
  0         0  
1403             } else {
1404 20         33 push @{$self->{waiting_clients}}, $cp;
  20         87  
1405             }
1406              
1407 20         51 $self->{waiting_client_count}++;
1408 20         80 $self->{waiting_client_map}{$cp->{fd}} = 1;
1409              
1410 20         92 $self->spawn_backends;
1411             }
1412              
1413             # sees if it should spawn one or more backend connections
1414             sub spawn_backends {
1415 171     171 0 370 my Perlbal::Service $self = shift;
1416              
1417             # check our lock and set it if we can
1418 171 50       782 return if $self->{spawn_lock};
1419 171         371 $self->{spawn_lock} = 1;
1420              
1421             # sanity checks on our bookkeeping
1422 171 50       624 if ($self->{pending_connect_count} < 0) {
1423 0         0 Perlbal::log('crit', "Bogus: service $self->{name} has pending connect ".
1424             "count of $self->{pending_connect_count}?! Resetting.");
1425 0 0       0 $self->{pending_connect_count} = scalar
1426 0         0 map { $_ && ! $_->{closed} } values %{$self->{pending_connects}};
  0         0  
1427             }
1428              
1429             # keep track of the sum of existing_bored + bored_created
1430 171         294 my $backends_created = scalar(@{$self->{bored_backends}}) + $self->{pending_connect_count};
  171         790  
1431 171         1122 my $backends_needed = $self->{waiting_client_count} + $self->{connect_ahead};
1432 171         328 my $to_create = $backends_needed - $backends_created;
1433              
1434 171         378 my $pool = $self->{pool};
1435              
1436             # can't create more than this, assuming one pending connect per node
1437 171 50       1333 my $max_creatable = $pool ? ($self->{pool}->node_count - $self->{pending_connect_count}) : 1;
1438 171 50       521 $to_create = $max_creatable if $to_create > $max_creatable;
1439              
1440             # cap number of attempted connects at once
1441 171 50       573 $to_create = 10 if $to_create > 10;
1442              
1443 171         361 my $now = time;
1444              
1445 171         651 while ($to_create > 0) {
1446 20         38 $to_create--;
1447              
1448             # spawn processes if not a pool, else whine.
1449 20 50       71 unless ($pool) {
1450 0 0       0 if (my $sp = $self->{server_process}) {
1451 0         0 warn "To create = $to_create...\n";
1452 0         0 warn " spawning $sp\n";
1453 0         0 my $be = Perlbal::BackendHTTP->new_process($self, $sp);
1454 0         0 return;
1455             }
1456 0         0 warn "No pool! Can't spawn backends.\n";
1457 0         0 return;
1458             }
1459              
1460 20         119 my ($ip, $port) = $self->{pool}->get_backend_endpoint;
1461 20 50       72 unless ($ip) {
1462 0         0 Perlbal::log('crit', "No backend IP for service $self->{name}");
1463             # FIXME: register desperate flag, so load-balancer module can callback when it has a node
1464 0         0 $self->{spawn_lock} = 0;
1465 0         0 return;
1466             }
1467              
1468             # handle retry timeouts so we don't spin
1469 20 50       115 next if $self->{backend_no_spawn}->{"$ip:$port"};
1470              
1471             # if it's pending, verify the pending one is still valid
1472 20 50       118 if (my Perlbal::BackendHTTP $be = $self->{pending_connects}{"$ip:$port"}) {
1473 0         0 my $age = $now - $be->{create_time};
1474 0 0 0     0 if ($age >= 5 && $be->{state} eq "connecting") {
    0 0        
    0          
1475 0         0 $be->close('connect_timeout');
1476             } elsif ($age >= 60 && $be->{state} eq "verifying_backend") {
1477             # after 60 seconds of attempting to verify, we're probably already dead
1478 0         0 $be->close('verify_timeout');
1479             } elsif (! $be->{closed}) {
1480 0         0 next;
1481             }
1482             }
1483              
1484             # now actually spawn a backend and add it to our pending list
1485 20 50       303 if (my $be = Perlbal::BackendHTTP->new($self, $ip, $port, { pool => $self->{pool} })) {
1486 20         107 $self->add_pending_connect($be);
1487             }
1488             }
1489              
1490             # clear our spawn lock
1491 171         1021 $self->{spawn_lock} = 0;
1492             }
1493              
1494             # getter only
1495             sub role {
1496 89     89 0 442 my Perlbal::Service $self = shift;
1497 89         3558 return $self->{role};
1498             }
1499              
1500             # called by BackendHTTP to ask if a client's IP is in our trusted list
1501             sub trusted_ip {
1502 431     431 0 9193 my Perlbal::Service $self = shift;
1503 431         773 my $ip = shift;
1504              
1505 431 50       2397 return 1 if $self->{'always_trusted'};
1506              
1507 431         1125 my $tmap = $self->{trusted_upstream_proxies};
1508 431 50       3519 return 0 unless $tmap;
1509              
1510             # try to use it as a Net::Netmask object
1511 0         0 for my $tmap (@{ $self->{trusted_upstream_proxies} }) {
  0         0  
1512 0 0       0 return 1 if eval { $tmap->match($ip); };
  0         0  
1513             }
1514 0         0 return 0;
1515             }
1516              
1517             # manage some header stuff
1518             sub header_management {
1519 2     2 0 4 my Perlbal::Service $self = shift;
1520 2         5 my ($mode, $key, $val, $mc) = @_;
1521 2 50       8 return $mc->err("no header provided") unless $key;
1522 2 50 33     17 return $mc->err("no value provided") unless
      33        
1523             (defined $val && length $val || $mode eq 'remove');
1524              
1525 2 50       8 if ($mode eq 'insert') {
    0          
1526 2         4 push @{$self->{extra_headers}->{insert}}, [ $key, $val ];
  2         10  
1527             } elsif ($mode eq 'remove') {
1528 0         0 push @{$self->{extra_headers}->{remove}}, $key;
  0         0  
1529             }
1530 2         9 return $mc->ok;
1531             }
1532              
1533             sub munge_headers {
1534 222     222 0 594 my Perlbal::Service $self = $_[0];
1535 222         354 my Perlbal::HTTPHeaders $hdrs = $_[1];
1536              
1537             # handle removals first
1538 222         836 foreach my $hdr (@{$self->{extra_headers}->{remove}}) {
  222         1751  
1539 0         0 $hdrs->header($hdr, undef);
1540             }
1541              
1542             # and now insertions
1543 222         463 foreach my $hdr (@{$self->{extra_headers}->{insert}}) {
  222         1493  
1544 0         0 $hdrs->header($hdr->[0], $hdr->[1]);
1545             }
1546             }
1547              
1548             # getter/setter
1549             sub selector {
1550 93     93 0 280 my Perlbal::Service $self = shift;
1551 93 100       514 if (@_) {
1552 4         7 my $ref = shift;
1553             $self->{selector} = sub {
1554 89     89   231 my $cb = shift;
1555              
1556             # try to give it to our defined selector
1557 89         535 my $res = $ref->($cb);
1558              
1559             # if that failed and we have a default, then give it to them
1560 89 50 66     613 if (!$res && $self->{default_service}) {
1561 0         0 $self->{default_service}->adopt_base_client($cb);
1562 0         0 return 1;
1563             }
1564              
1565 89         961 return $res;
1566 4         26 };
1567             }
1568 93         544 return $self->{selector};
1569             }
1570              
1571             # This is called anytime a client is leaving this service to be another service.
1572             sub release_client {
1573 87     87 0 188 my Perlbal::Service $self = shift;
1574 87         152 my Perlbal::ClientHTTPBase $cb = shift;
1575              
1576 87         2551 $self->munge_headers($cb->{req_headers});
1577 87         239 return;
1578             }
1579              
1580             # given a base client from a 'selector' role, down-cast it to its specific type
1581             sub adopt_base_client {
1582 87     87 0 163 my Perlbal::Service $self = shift;
1583 87         162 my Perlbal::ClientHTTPBase $cb = shift;
1584              
1585 87 50       697 if (my $orig_service = $cb->{service}) {
1586 87         602 $orig_service->release_client($cb);
1587             }
1588              
1589 87         176 $cb->{service} = $self;
1590              
1591 87 100       1704 if ($self->{'role'} eq "web_server") {
    100          
    100          
1592 7         163 Perlbal::ClientHTTP->new_from_base($cb);
1593 7         30 return;
1594             } elsif ($self->{'role'} eq "reverse_proxy") {
1595 55         544 Perlbal::ClientProxy->new_from_base($cb);
1596 55         4863 return;
1597             } elsif ($self->{'role'} eq "selector") {
1598 24         170 Perlbal::ClientHTTPBase->new_from_base($cb);
1599 24         72 return;
1600             } else {
1601 1         11 $cb->_simple_response(500, "Can't map to service type $self->{'role'}");
1602             }
1603             }
1604              
1605             # turn a ClientProxy or ClientHTTP back into a generic base client
1606             # (for a service-selector role)
1607             sub return_to_base {
1608 64     64 0 362 my Perlbal::Service $self = shift;
1609 64         114 my Perlbal::ClientHTTPBase $cb = shift; # actually a subclass of Perlbal::ClientHTTPBase
1610              
1611 64         371 $cb->{service} = $self;
1612 64         527 Perlbal::Util::rebless($cb, "Perlbal::ClientHTTPBase");
1613              
1614             # the read/watch events are reset by ClientHTTPBase's http_response_sent (our caller)
1615             }
1616              
1617             # Service
1618             sub enable {
1619 42     42 0 79 my Perlbal::Service $self;
1620             my $mc;
1621              
1622 42         97 ($self, $mc) = @_;
1623              
1624 42 50       170 if ($self->{enabled}) {
1625 0 0       0 $mc && $mc->err("service $self->{name} is already enabled");
1626 0         0 return 0;
1627             }
1628              
1629 42         68 my $listener;
1630              
1631             # create UDP upload tracker listener
1632 42 50       201 if ($self->{role} eq "upload_tracker") {
1633 0         0 $listener = Perlbal::UploadListener->new($self->{listen}, $self);
1634             }
1635              
1636             # create TCP listening socket
1637 42 100 66     655 if (! $listener && $self->{listen}) {
1638 39         91 my $opts = {};
1639 39 50       167 if ($self->{enable_ssl}) {
1640 0 0       0 $opts->{ssl} = {
    0          
1641             SSL_key_file => $self->{ssl_key_file},
1642             SSL_cert_file => $self->{ssl_cert_file},
1643             SSL_cipher_list => $self->{ssl_cipher_list},
1644             (defined $self->{ssl_ca_path} ? (SSL_ca_path => $self->{ssl_ca_path}) : ()),
1645             (defined $self->{ssl_verify_mode} ? (SSL_verify_mode => $self->{ssl_verify_mode}) : ()),
1646             };
1647 0 0       0 return $mc->err("IO::Socket:SSL (0.98+) not available. Can't do SSL.") unless eval "use IO::Socket::SSL 0.98 (); 1;";
1648 0 0       0 return $mc->err("SSL key file ($self->{ssl_key_file}) doesn't exist") unless -f $self->{ssl_key_file};
1649 0 0       0 return $mc->err("SSL cert file ($self->{ssl_cert_file}) doesn't exist") unless -f $self->{ssl_cert_file};
1650             }
1651              
1652 39         397 my $tl = Perlbal::TCPListener->new($self->{listen}, $self, $opts);
1653 39 50       150 unless ($tl) {
1654 0 0       0 $mc && $mc->err("Can't start service '$self->{name}' on $self->{listen}: $Perlbal::last_error");
1655 0         0 return 0;
1656             }
1657 39         102 $listener = $tl;
1658             }
1659              
1660 42         114 $self->{listener} = $listener;
1661 42         91 $self->{enabled} = 1;
1662 42 50       232 return $mc ? $mc->ok : 1;
1663             }
1664              
1665             # Service
1666             sub disable {
1667 0     0 0 0 my Perlbal::Service $self;
1668 0         0 my ($mc, $force);
1669              
1670 0         0 ($self, $mc, $force) = @_;
1671              
1672 0 0       0 if (! $self->{enabled}) {
1673 0 0       0 $mc && $mc->err("service $self->{name} is already disabled");
1674 0         0 return 0;
1675             }
1676 0 0 0     0 if ($self->{role} eq "management" && ! $force) {
1677 0 0       0 $mc && $mc->err("can't disable management service");
1678 0         0 return 0;
1679             }
1680              
1681             # find listening socket
1682 0         0 my $tl = $self->{listener};
1683 0 0       0 $tl->close if $tl;
1684 0         0 $self->{listener} = undef;
1685 0         0 $self->{enabled} = 0;
1686 0 0       0 return $mc ? $mc->ok : 1;
1687             }
1688              
1689             sub stats_info
1690             {
1691 0     0 0 0 my Perlbal::Service $self = shift;
1692 0         0 my $out = shift;
1693 0         0 my $now = time;
1694              
1695 0         0 $out->("SERVICE $self->{name}");
1696 0   0     0 $out->(" listening: " . ($self->{listen} || "--"));
1697 0         0 $out->(" role: $self->{role}");
1698 0 0 0     0 if ($self->{role} eq "reverse_proxy" ||
1699             $self->{role} eq "web_server") {
1700 0         0 $out->(" pend clients: $self->{waiting_client_count}");
1701 0         0 $out->(" pend backend: $self->{pending_connect_count}");
1702 0         0 foreach my $ipport (sort keys %{$self->{pending_connects}}) {
  0         0  
1703 0         0 my $be = $self->{pending_connects}{$ipport};
1704 0 0       0 next unless $be;
1705 0         0 my $age = $now - $be->{create_time};
1706 0 0       0 $out->(" $ipport - " . ($be->{closed} ? "(closed)" : $be->{state}) . " - ${age}s");
1707             }
1708             }
1709 0 0       0 if ($self->{role} eq "reverse_proxy") {
    0          
1710 0 0       0 if ($self->{reproxy_cache}) {
1711 0   0     0 my $hits = $self->{_stat_cache_hits} || 0;
1712 0   0     0 my $hit_rate = sprintf("%0.02f%%", eval { $hits / ($self->{_stat_requests} || 0) * 100 } || 0);
1713              
1714 0         0 my $size = eval { $self->{reproxy_cache}->size };
  0         0  
1715 0 0       0 $size = defined($size) ? $size : 'undef';
1716              
1717 0         0 my $maxsize = eval { $self->{reproxy_cache}->maxsize };
  0         0  
1718 0 0       0 $maxsize = defined ($maxsize) ? $maxsize : 'undef';
1719              
1720 0   0     0 my $sizepercent = eval { sprintf("%0.02f%%", $size / $maxsize * 100) } || 'undef';
1721              
1722 0         0 $out->(" cache size: $size/$maxsize ($sizepercent)");
1723 0         0 $out->(" cache hits: $hits");
1724 0         0 $out->("cache hit rate: $hit_rate");
1725             }
1726              
1727 0         0 my $bored_count = scalar @{$self->{bored_backends}};
  0         0  
1728 0         0 $out->(" connect-ahead: $bored_count/$self->{connect_ahead}");
1729 0 0       0 if ($self->{pool}) {
1730 0         0 $out->(" pool: " . $self->{pool}->name);
1731 0         0 $out->(" nodes:");
1732 0         0 foreach my $n (@{ $self->{pool}->nodes }) {
  0         0  
1733 0         0 my $hostport = "$n->[0]:$n->[1]";
1734 0   0     0 $out->(sprintf(" %-21s %7d", $hostport, $self->{pool}->node_used($hostport) || 0));
1735             }
1736             }
1737             } elsif ($self->{role} eq "web_server") {
1738 0         0 $out->(" docroot: $self->{docroot}");
1739             }
1740             }
1741              
1742             # simple passthroughs to the run_hook mechanism. part of the reportto interface.
1743             sub backend_response_received {
1744 134     134 0 948 return $_[0]->run_hook('backend_response_received', $_[1]);
1745             }
1746              
1747             # just a getter for our name
1748             sub name {
1749 29     29 0 77 my Perlbal::Service $self = $_[0];
1750 29         199 return $self->{name};
1751             }
1752              
1753             sub listenaddr {
1754 29     29 0 62 my Perlbal::Service $self = $_[0];
1755 29         286 return $self->{listen};
1756             }
1757              
1758             sub reproxy_cache {
1759 0     0 0 0 my Perlbal::Service $self = $_[0];
1760 0         0 return $self->{reproxy_cache};
1761             }
1762              
1763             sub add_to_reproxy_url_cache {
1764 2     2 0 4 my Perlbal::Service $self;
1765 2         3 my ($reqhd, $reshd);
1766              
1767 2         6 ($self, $reqhd, $reshd) = @_;
1768              
1769             # is caching enabled on this service?
1770 2 50       10 my $cache = $self->{reproxy_cache} or
1771             return 0;
1772              
1773             # these should always be set anyway, from BackendHTTP:
1774 2 50       6 my $reproxy_cache_for = $reshd->header('X-REPROXY-CACHE-FOR') or return 0;
1775 2 50       8 my $urls = $reshd->header('X-REPROXY-URL') or return 0;
1776              
1777 2         8 my ($timeout_delta, $cache_headers) = split ';', $reproxy_cache_for, 2;
1778 2 50       9 my $timeout = $timeout_delta ? time() + $timeout_delta : undef;
1779              
1780 2   50     7 my $hostname = $reqhd->header("Host") || '';
1781 2   50     8 my $requri = $reqhd->request_uri || '';
1782 2         7 my $key = "$hostname|$requri";
1783              
1784 2         4 my @headers;
1785 2         7 foreach my $header (split /\s+/, $cache_headers) {
1786 6         8 my $value;
1787 6 100 66     24 next unless $header && ($value = $reshd->header($header));
1788 4 100       13 $value = _ref_to($value) if uc($header) eq 'CONTENT-TYPE';
1789 4         10 push @headers, _ref_to($header), $value;
1790             }
1791              
1792 2         15 $cache->set($key, [$timeout, \@headers, $urls]);
1793             }
1794              
1795             # given a string, return a shared reference to that string. to save
1796             # memory when lots of same string is stored.
1797             my %refs;
1798             sub _ref_to {
1799 6     6   7 my $key = shift;
1800 6   100     29 return $refs{$key} || ($refs{$key} = \$key);
1801             }
1802              
1803             1;
1804              
1805             # Local Variables:
1806             # mode: perl
1807             # c-basic-indent: 4
1808             # indent-tabs-mode: nil
1809             # End: