| 1 | ###################################################################### |
|---|
| 2 | # Pool class |
|---|
| 3 | ###################################################################### |
|---|
| 4 | # |
|---|
| 5 | # Copyright 2004, Danga Interactive, Inc. |
|---|
| 6 | # Copyright 2005-2007, Six Apart, Ltd. |
|---|
| 7 | # |
|---|
| 8 | |
|---|
| 9 | package Perlbal::Pool; |
|---|
| 10 | use strict; |
|---|
| 11 | use warnings; |
|---|
| 12 | |
|---|
| 13 | use Perlbal::BackendHTTP; |
|---|
| 14 | |
|---|
| 15 | # how often to reload the nodefile |
|---|
| 16 | use constant NODEFILE_RELOAD_FREQ => 3; |
|---|
| 17 | |
|---|
| 18 | # balance methods we support (note: sendstats mode is now removed) |
|---|
| 19 | use constant BM_ROUNDROBIN => 2; |
|---|
| 20 | use constant BM_RANDOM => 3; |
|---|
| 21 | |
|---|
| 22 | use fields ( |
|---|
| 23 | 'name', # string; name of this pool |
|---|
| 24 | 'use_count', # int; number of services using us |
|---|
| 25 | 'nodes', # arrayref; [ip, port] values (port defaults to 80) |
|---|
| 26 | 'node_count', # int; number of nodes |
|---|
| 27 | 'node_used', # hashref; { ip:port => use count } |
|---|
| 28 | 'balance_method', # int; BM_ constant from above |
|---|
| 29 | |
|---|
| 30 | # used in nodefile mode |
|---|
| 31 | 'nodefile', # string; filename to read nodes from |
|---|
| 32 | 'nodefile.lastmod', # unix time nodefile was last modified |
|---|
| 33 | 'nodefile.lastcheck', # unix time nodefile was last stated |
|---|
| 34 | 'nodefile.checking', # boolean; if true AIO is stating the file for us |
|---|
| 35 | ); |
|---|
| 36 | |
|---|
| 37 | sub new { |
|---|
| 38 | my Perlbal::Pool $self = shift; |
|---|
| 39 | $self = fields::new($self) unless ref $self; |
|---|
| 40 | |
|---|
| 41 | my ($name) = @_; |
|---|
| 42 | |
|---|
| 43 | $self->{name} = $name; |
|---|
| 44 | $self->{use_count} = 0; |
|---|
| 45 | |
|---|
| 46 | $self->{nodes} = []; |
|---|
| 47 | $self->{node_count} = 0; |
|---|
| 48 | $self->{node_used} = {}; |
|---|
| 49 | |
|---|
| 50 | $self->{nodefile} = undef; |
|---|
| 51 | $self->{balance_method} = BM_RANDOM; |
|---|
| 52 | |
|---|
| 53 | return $self; |
|---|
| 54 | } |
|---|
| 55 | |
|---|
| 56 | sub set { |
|---|
| 57 | my Perlbal::Pool $self = shift; |
|---|
| 58 | |
|---|
| 59 | my ($key, $val, $mc) = @_; |
|---|
| 60 | my $set = sub { $self->{$key} = $val; return $mc->ok; }; |
|---|
| 61 | |
|---|
| 62 | if ($key eq 'nodefile') { |
|---|
| 63 | # allow to unset it, which stops us from checking it further, |
|---|
| 64 | # but doesn't clear our current list of nodes |
|---|
| 65 | if ($val =~ /^(?:none|undef|null|""|'')$/) { |
|---|
| 66 | $self->{'nodefile'} = undef; |
|---|
| 67 | $self->{'nodefile.lastmod'} = 0; |
|---|
| 68 | $self->{'nodefile.checking'} = 0; |
|---|
| 69 | $self->{'nodefile.lastcheck'} = 0; |
|---|
| 70 | return $mc->ok; |
|---|
| 71 | } |
|---|
| 72 | |
|---|
| 73 | # enforce that it exists from here on out |
|---|
| 74 | return $mc->err("File not found") |
|---|
| 75 | unless -e $val; |
|---|
| 76 | |
|---|
| 77 | # force a reload |
|---|
| 78 | $self->{'nodefile'} = $val; |
|---|
| 79 | $self->{'nodefile.lastmod'} = 0; |
|---|
| 80 | $self->{'nodefile.checking'} = 0; |
|---|
| 81 | $self->load_nodefile; |
|---|
| 82 | $self->{'nodefile.lastcheck'} = time; |
|---|
| 83 | return $mc->ok; |
|---|
| 84 | } |
|---|
| 85 | |
|---|
| 86 | if ($key eq "balance_method") { |
|---|
| 87 | $val = { |
|---|
| 88 | 'random' => BM_RANDOM, |
|---|
| 89 | }->{$val}; |
|---|
| 90 | return $mc->err("Unknown balance method") |
|---|
| 91 | unless $val; |
|---|
| 92 | return $set->(); |
|---|
| 93 | } |
|---|
| 94 | |
|---|
| 95 | } |
|---|
| 96 | |
|---|
| 97 | sub dumpconfig { |
|---|
| 98 | my Perlbal::Pool $self = shift; |
|---|
| 99 | my $name = $self->{name}; |
|---|
| 100 | |
|---|
| 101 | my @return; |
|---|
| 102 | |
|---|
| 103 | if (my $nodefile = $self->{'nodefile'}) { |
|---|
| 104 | push @return, "SET nodefile = $nodefile"; |
|---|
| 105 | } else { |
|---|
| 106 | foreach my $node (@{$self->{nodes}}) { |
|---|
| 107 | my ($ip, $port) = @$node; |
|---|
| 108 | push @return, "POOL ADD $name $ip:$port"; |
|---|
| 109 | } |
|---|
| 110 | } |
|---|
| 111 | return @return; |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | # returns string of balance method |
|---|
| 115 | sub balance_method { |
|---|
| 116 | my Perlbal::Pool $self = $_[0]; |
|---|
| 117 | my $methods = { |
|---|
| 118 | &BM_ROUNDROBIN => "round_robin", |
|---|
| 119 | &BM_RANDOM => "random", |
|---|
| 120 | }; |
|---|
| 121 | return $methods->{$self->{balance_method}} || $self->{balance_method}; |
|---|
| 122 | } |
|---|
| 123 | |
|---|
| 124 | sub load_nodefile { |
|---|
| 125 | my Perlbal::Pool $self = shift; |
|---|
| 126 | return 0 unless $self->{'nodefile'}; |
|---|
| 127 | |
|---|
| 128 | if ($Perlbal::OPTMOD_LINUX_AIO) { |
|---|
| 129 | return $self->_load_nodefile_async; |
|---|
| 130 | } else { |
|---|
| 131 | return $self->_load_nodefile_sync; |
|---|
| 132 | } |
|---|
| 133 | } |
|---|
| 134 | |
|---|
| 135 | sub _parse_nodefile { |
|---|
| 136 | my Perlbal::Pool $self = shift; |
|---|
| 137 | my $dataref = shift; |
|---|
| 138 | |
|---|
| 139 | my @nodes = split(/\r?\n/, $$dataref); |
|---|
| 140 | |
|---|
| 141 | # prepare for adding nodes |
|---|
| 142 | $self->{nodes} = []; |
|---|
| 143 | $self->{node_used} = {}; |
|---|
| 144 | |
|---|
| 145 | foreach (@nodes) { |
|---|
| 146 | s/\#.*//; |
|---|
| 147 | if (/(\d+\.\d+\.\d+\.\d+)(?::(\d+))?/) { |
|---|
| 148 | my ($ip, $port) = ($1, $2); |
|---|
| 149 | $port ||= 80; |
|---|
| 150 | $self->{node_used}->{"$ip:$port"} ||= 0; # set to 0 if not set |
|---|
| 151 | push @{$self->{nodes}}, [ $ip, $port ]; |
|---|
| 152 | } |
|---|
| 153 | } |
|---|
| 154 | |
|---|
| 155 | # setup things using new data |
|---|
| 156 | $self->{node_count} = scalar @{$self->{nodes}}; |
|---|
| 157 | } |
|---|
| 158 | |
|---|
| 159 | sub _load_nodefile_sync { |
|---|
| 160 | my Perlbal::Pool $self = shift; |
|---|
| 161 | |
|---|
| 162 | my $mod = (stat($self->{nodefile}))[9]; |
|---|
| 163 | return if $mod == $self->{'nodefile.lastmod'}; |
|---|
| 164 | $self->{'nodefile.lastmod'} = $mod; |
|---|
| 165 | |
|---|
| 166 | open NODEFILE, $self->{nodefile} or return; |
|---|
| 167 | my $nodes; |
|---|
| 168 | { local $/ = undef; $nodes = <NODEFILE>; } |
|---|
| 169 | close NODEFILE; |
|---|
| 170 | $self->_parse_nodefile(\$nodes); |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | sub _load_nodefile_async { |
|---|
| 174 | my Perlbal::Pool $self = shift; |
|---|
| 175 | |
|---|
| 176 | return if $self->{'nodefile.checking'}; |
|---|
| 177 | $self->{'nodefile.checking'} = 1; |
|---|
| 178 | |
|---|
| 179 | Perlbal::AIO::aio_stat($self->{nodefile}, sub { |
|---|
| 180 | $self->{'nodefile.checking'} = 0; |
|---|
| 181 | |
|---|
| 182 | # this might have gotten unset while we were out statting the file, which |
|---|
| 183 | # means that the user has instructed us not to use a node file, and may |
|---|
| 184 | # have changed the nodes in the pool, so we should do nothing and return |
|---|
| 185 | return unless $self->{'nodefile'}; |
|---|
| 186 | |
|---|
| 187 | # ignore if the file doesn't exist |
|---|
| 188 | return unless -e _; |
|---|
| 189 | |
|---|
| 190 | my $mod = (stat(_))[9]; |
|---|
| 191 | return if $mod == $self->{'nodefile.lastmod'}; |
|---|
| 192 | $self->{'nodefile.lastmod'} = $mod; |
|---|
| 193 | |
|---|
| 194 | # construct a filehandle (we only have a descriptor here) |
|---|
| 195 | open NODEFILE, $self->{nodefile} |
|---|
| 196 | or return; |
|---|
| 197 | my $nodes; |
|---|
| 198 | { local $/ = undef; $nodes = <NODEFILE>; } |
|---|
| 199 | close NODEFILE; |
|---|
| 200 | |
|---|
| 201 | $self->_parse_nodefile(\$nodes); |
|---|
| 202 | return; |
|---|
| 203 | }); |
|---|
| 204 | |
|---|
| 205 | return 1; |
|---|
| 206 | } |
|---|
| 207 | |
|---|
| 208 | sub add { |
|---|
| 209 | my Perlbal::Pool $self = shift; |
|---|
| 210 | my ($ip, $port) = @_; |
|---|
| 211 | |
|---|
| 212 | $self->remove($ip, $port); # no dupes |
|---|
| 213 | |
|---|
| 214 | $self->{node_used}->{"$ip:$port"} = 0; |
|---|
| 215 | push @{$self->{nodes}}, [ $ip, $port ]; |
|---|
| 216 | $self->{node_count} = scalar(@{$self->{nodes}}); |
|---|
| 217 | } |
|---|
| 218 | |
|---|
| 219 | sub remove { |
|---|
| 220 | my Perlbal::Pool $self = shift; |
|---|
| 221 | my ($ip, $port) = @_; |
|---|
| 222 | |
|---|
| 223 | delete $self->{node_used}->{"$ip:$port"}; |
|---|
| 224 | @{$self->{nodes}} = grep { "$_->[0]:$_->[1]" ne "$ip:$port" } @{$self->{nodes}}; |
|---|
| 225 | $self->{node_count} = scalar(@{$self->{nodes}}); |
|---|
| 226 | } |
|---|
| 227 | |
|---|
| 228 | sub get_backend_endpoint { |
|---|
| 229 | my Perlbal::Pool $self = $_[0]; |
|---|
| 230 | |
|---|
| 231 | my @endpoint; # (IP,port) |
|---|
| 232 | |
|---|
| 233 | # re-load nodefile if necessary |
|---|
| 234 | if ($self->{nodefile}) { |
|---|
| 235 | my $now = time; |
|---|
| 236 | if ($now > $self->{'nodefile.lastcheck'} + NODEFILE_RELOAD_FREQ) { |
|---|
| 237 | $self->{'nodefile.lastcheck'} = $now; |
|---|
| 238 | $self->load_nodefile; |
|---|
| 239 | } |
|---|
| 240 | } |
|---|
| 241 | |
|---|
| 242 | # no nodes? |
|---|
| 243 | return () unless $self->{node_count}; |
|---|
| 244 | |
|---|
| 245 | # pick one randomly |
|---|
| 246 | return @{$self->{nodes}[int(rand($self->{node_count}))]}; |
|---|
| 247 | } |
|---|
| 248 | |
|---|
| 249 | sub backend_should_live { |
|---|
| 250 | my Perlbal::Pool $self = $_[0]; |
|---|
| 251 | my Perlbal::BackendHTTP $be = $_[1]; |
|---|
| 252 | |
|---|
| 253 | # a backend stays alive if we still have users. eventually this whole |
|---|
| 254 | # function might do more and actually take into account the individual |
|---|
| 255 | # backend, but for now, this suits us. |
|---|
| 256 | return 1 if $self->{use_count}; |
|---|
| 257 | return 0; |
|---|
| 258 | } |
|---|
| 259 | |
|---|
| 260 | sub node_count { |
|---|
| 261 | my Perlbal::Pool $self = $_[0]; |
|---|
| 262 | return $self->{node_count}; |
|---|
| 263 | } |
|---|
| 264 | |
|---|
| 265 | sub nodes { |
|---|
| 266 | my Perlbal::Pool $self = $_[0]; |
|---|
| 267 | return $self->{nodes}; |
|---|
| 268 | } |
|---|
| 269 | |
|---|
| 270 | sub node_used { |
|---|
| 271 | my Perlbal::Pool $self = $_[0]; |
|---|
| 272 | return $self->{node_used}->{$_[1]}; |
|---|
| 273 | } |
|---|
| 274 | |
|---|
| 275 | sub mark_node_used { |
|---|
| 276 | my Perlbal::Pool $self = $_[0]; |
|---|
| 277 | $self->{node_used}->{$_[1]}++; |
|---|
| 278 | } |
|---|
| 279 | |
|---|
| 280 | sub increment_use_count { |
|---|
| 281 | my Perlbal::Pool $self = $_[0]; |
|---|
| 282 | $self->{use_count}++; |
|---|
| 283 | } |
|---|
| 284 | |
|---|
| 285 | sub decrement_use_count { |
|---|
| 286 | my Perlbal::Pool $self = $_[0]; |
|---|
| 287 | $self->{use_count}--; |
|---|
| 288 | } |
|---|
| 289 | |
|---|
| 290 | sub name { |
|---|
| 291 | my Perlbal::Pool $self = $_[0]; |
|---|
| 292 | return $self->{name}; |
|---|
| 293 | } |
|---|
| 294 | |
|---|
| 295 | 1; |
|---|
| 296 | |
|---|
| 297 | # Local Variables: |
|---|
| 298 | # mode: perl |
|---|
| 299 | # c-basic-indent: 4 |
|---|
| 300 | # indent-tabs-mode: nil |
|---|
| 301 | # End: |
|---|