1#!/usr/bin/env perl 2 3# NOTE: These tests cover the act of reloading the configuration; changing 4# backends, pools, routes, etc. It doesn't cover ensuring the code of the main 5# file changes naturally, which is fine: there isn't any real way that can 6# fail and it can be covered specifically in a different test file. 7 8use strict; 9use warnings; 10use Test::More; 11use FindBin qw($Bin); 12use lib "$Bin/lib"; 13use Carp qw(croak); 14use MemcachedTest; 15use IO::Select; 16use IO::Socket qw(AF_INET SOCK_STREAM); 17 18# TODO: possibly... set env var to a generated temp filename before starting 19# the server so we can pass that in? 20my $modefile = "/tmp/proxyconfigmode.lua"; 21 22if (!supports_proxy()) { 23 plan skip_all => 'proxy not enabled'; 24 exit 0; 25} 26 27# Set up some server sockets. 28sub mock_server { 29 my $port = shift; 30 my $srv = IO::Socket->new( 31 Domain => AF_INET, 32 Type => SOCK_STREAM, 33 Proto => 'tcp', 34 LocalHost => '127.0.0.1', 35 LocalPort => $port, 36 ReusePort => 1, 37 Listen => 5) || die "IO::Socket: $@"; 38 return $srv; 39} 40 41sub accept_backend { 42 my $srv = shift; 43 my $be = $srv->accept(); 44 $be->autoflush(1); 45 ok(defined $be, "mock backend created"); 46 like(<$be>, qr/version/, "received version command"); 47 print $be "VERSION 1.0.0-mock\r\n"; 48 49 return $be; 50} 51 52# Put a version command down the pipe to ensure the socket is clear. 53# client version commands skip the proxy code 54sub check_version { 55 my $ps = shift; 56 print $ps "version\r\n"; 57 like(<$ps>, qr/VERSION /, "version received"); 58} 59 60sub write_modefile { 61 my $cmd = shift; 62 open(my $fh, "> $modefile") or die "Couldn't overwrite $modefile: $!"; 63 print $fh $cmd; 64 close($fh); 65} 66 67sub wait_reload { 68 my $w = shift; 69 like(<$w>, qr/ts=(\S+) gid=\d+ type=proxy_conf status=start/, "reload started"); 70 like(<$w>, qr/ts=(\S+) gid=\d+ type=proxy_conf status=done/, "reload completed"); 71} 72 73# Not looking for a clear pipeline, just when a reload finishes. 74sub wait_reload_relaxed { 75 my $w = shift; 76 while (my $line = <$w>) { 77 if ($line =~ m/type=proxy_conf status=done/) { 78 last; 79 } 80 } 81 pass("reload complete"); 82} 83 84my @mocksrvs = (); 85#diag "making mock servers"; 86for my $port (11511, 11512, 11513) { 87 my $srv = mock_server($port); 88 ok(defined $srv, "mock server created"); 89 push(@mocksrvs, $srv); 90} 91 92diag "testing failure to start"; 93write_modefile("invalid syntax"); 94eval { 95 my $p_srv = new_memcached('-o proxy_config=./t/proxyconfig.lua'); 96}; 97ok($@ && $@ =~ m/Failed to connect/, "server successfully not started"); 98 99write_modefile('return "none"'); 100my $p_srv = new_memcached('-o proxy_config=./t/proxyconfig.lua'); 101my $ps = $p_srv->sock; 102$ps->autoflush(1); 103 104# Create a watcher so we can monitor when reloads complete. 105my $watcher = $p_srv->new_sock; 106print $watcher "watch proxyevents\n"; 107is(<$watcher>, "OK\r\n", "watcher enabled"); 108 109{ 110 # test with stubbed main routes. 111 print $ps "mg foo v\r\n"; 112 is(scalar <$ps>, "SERVER_ERROR no mg route\r\n", "no mg route loaded"); 113} 114 115# Load some backends 116{ 117 write_modefile('return "start"'); 118 119 $p_srv->reload(); 120 wait_reload($watcher); 121} 122 123my @mbe = (); 124# A map of where keys route to for worker IO tests later 125my %keymap = (); 126my $keycount = 100; 127{ 128 # set up server backend sockets. 129 my $s = IO::Select->new(); 130 for my $msrv ($mocksrvs[0], $mocksrvs[1], $mocksrvs[2]) { 131 my $be = accept_backend($msrv); 132 $s->add($be); 133 push(@mbe, $be); 134 } 135 136 # Try sending something. 137 my $cmd = "mg foo v\r\n"; 138 print $ps $cmd; 139 my @readable = $s->can_read(0.25); 140 is(scalar @readable, 1, "only one backend became readable after mg"); 141 my $be = shift @readable; 142 is(scalar <$be>, $cmd, "metaget passthrough"); 143 print $be "EN\r\n"; 144 is(scalar <$ps>, "EN\r\n", "miss received"); 145 146 # Route a bunch of keys and map them to backends. 147 for my $key (0 .. $keycount) { 148 print $ps "mg /test/$key\r\n"; 149 my @readable = $s->can_read(0.25); 150 is(scalar @readable, 1, "only one backend became readable for this key"); 151 my $be = shift @readable; 152 for (0 .. 2) { 153 if ($be == $mbe[$_]) { 154 $keymap{$key} = $_; 155 } 156 } 157 is(scalar <$be>, "mg /test/$key\r\n", "got mg passthrough"); 158 print $be "EN\r\n"; 159 is(scalar <$ps>, "EN\r\n", "miss received"); 160 } 161} 162 163# Test backend table arguments and per-backend time overrides 164my @holdbe = (); # avoid having the backends immediately disconnect and pollute log lines. 165{ 166 # This should create three new backend sockets 167 write_modefile('return "betable"'); 168 $p_srv->reload(); 169 wait_reload($watcher); 170 171 my $watch_s = IO::Select->new(); 172 $watch_s->add($watcher); 173 174 my $s = IO::Select->new(); 175 for my $msrv (@mocksrvs) { 176 $s->add($msrv); 177 } 178 my @readable = $s->can_read(0.25); 179 # All three backends should have changed despite having the same label, 180 # host, and port arguments. 181 is(scalar @readable, 3, "all listeners became readable"); 182 183 my @watchable = $watch_s->can_read(5); 184 is(scalar @watchable, 1, "got new watcher log lines"); 185 like(<$watcher>, qr/ts=(\S+) gid=\d+ type=proxy_backend error=readvalidate name=\S+ port=11511/, "one backend timed out connecting"); 186 like(<$watcher>, qr/ts=(\S+) gid=\d+ type=proxy_backend error=markedbad name=\S+ port=11511/, "backend was marked bad"); 187 188 for my $msrv (@readable) { 189 my $be = accept_backend($msrv); 190 push(@holdbe, $be); 191 } 192 193 # reload again and ensure no sockets become readable 194 $p_srv->reload(); 195 wait_reload($watcher); 196 @readable = $s->can_read(0.5); 197 is(scalar @readable, 0, "no new sockets"); 198} 199 200#diag "testing multiple connections"; 201{ 202 write_modefile('return "connections"'); 203 $p_srv->reload(); 204 wait_reload($watcher); 205 206 # Should get 3 new connetions for the first server. 207 my $msrv = $mocksrvs[0]; 208 my @bes = (); 209 for (1 .. 3) { 210 my $be = $msrv->accept(); 211 $be->autoflush(1); 212 ok(defined $be, "mock backend created"); 213 push(@bes, $be); 214 } 215 216 my $s = IO::Select->new(); 217 218 for my $be (@bes) { 219 $s->add($be); 220 like(<$be>, qr/version/, "received version command"); 221 print $be "VERSION 1.0.0-mock\r\n"; 222 } 223 224 # Command should only go to the first socket we created in N tries 225 for (1 .. 5) { 226 my $cmd = "mg foo$_ v\r\n"; 227 print $ps $cmd; 228 my @readable = $s->can_read(0.25); 229 my $be = $bes[0]; 230 is(scalar <$be>, $cmd, "get passthrough"); 231 print $be "EN\r\n"; 232 is(scalar <$ps>, "EN\r\n", "miss received"); 233 } 234 my @readable = $s->can_read(0.25); 235 is(scalar @readable, 0, "rest of connections are idle still"); 236 237 # Pipelined commands should all go to the first socket 238 print $ps "mg f1 v\r\nmg f2 v\r\nmg f3 v\r\n"; 239 @readable = $s->can_read(0.25); 240 is(scalar @readable, 1, "only one backend woke up"); 241 { 242 my $be = $bes[0]; 243 for (1 .. 3) { 244 is(scalar <$be>, "mg f$_ v\r\n", "mg to connection $_"); 245 print $be "EN\r\n"; 246 } 247 for (1 .. 3) { 248 is(scalar <$ps>, "EN\r\n", "miss $_ from backend"); 249 } 250 } 251 252 # Rest of sockets should be used when backend depth is nonzero 253 # need two more client sockets. 254 # client will be in conn_iowait, so if we write more requests down the 255 # same socket it won't go anywhere. 256 my $ps2 = $p_srv->new_sock; 257 my $ps3 = $p_srv->new_sock; 258 my @psocks = ($ps, $ps2, $ps3); 259 for (1 .. 3) { 260 my $psc = $psocks[$_ - 1]; 261 my $be = $bes[$_ - 1]; 262 print $psc "mg f$_ v\r\n"; 263 is(scalar <$be>, "mg f$_ v\r\n", "trying all connections"); 264 } 265 for my $be (@bes) { 266 print $be "EN\r\n"; 267 } 268 for my $psc (@psocks) { 269 is(scalar <$psc>, "EN\r\n", "miss from backend"); 270 } 271 272 # Verify the backend changes if we only change the connection count. 273 write_modefile('return "connectionsreload"'); 274 $p_srv->reload(); 275 wait_reload($watcher); 276 277 my $ms = IO::Select->new(); 278 $ms->add($msrv); 279 @readable = $ms->can_read(0.25); 280 is(scalar @readable, 1, "listener became readable after changing conncount"); 281 $bes[0] = accept_backend($readable[0]); 282} 283 284{ 285 note("Testing down backends"); 286 $watcher = $p_srv->new_sock; 287 print $watcher "watch proxyevents\n"; 288 is(<$watcher>, "OK\r\n", "watcher enabled"); 289 290 # Make a dedicated mock server for the down host. 291 my $msrv = mock_server(11517); 292 293 write_modefile('return "down"'); 294 $p_srv->reload(); 295 wait_reload_relaxed($watcher); 296 297 my $ms = IO::Select->new(); 298 $ms->add($msrv); 299 my @readable = $ms->can_read(0.25); 300 is(scalar @readable, 0, "listener did not become readable for down backend"); 301 302 print $ps "mg toast\r\n"; 303 is(scalar <$ps>, "SERVER_ERROR backend failure\r\n", "client received SERVER_ERROR"); 304 305 write_modefile('return "notdown"'); 306 $p_srv->reload(); 307 wait_reload_relaxed($watcher); 308 309 @readable = $ms->can_read(0.25); 310 is(scalar @readable, 1, "listener did become readable for backend that was down"); 311 312 my $be = accept_backend($readable[0]); 313 314 print $ps "mg toast\r\n"; 315 is(scalar <$be>, "mg toast\r\n", "backend works"); 316 print $be "HD\r\n"; 317 is(scalar <$ps>, "HD\r\n", "backned to client works"); 318 319 check_version($ps); 320} 321 322# Disconnect the existing sockets 323@mbe = (); 324@holdbe = (); 325@mocksrvs = (); 326$watcher = $p_srv->new_sock; 327# Reset the watcher and let logs die off. 328sleep 1; 329print $watcher "watch proxyevents\n"; 330is(<$watcher>, "OK\r\n", "watcher enabled"); 331 332{ 333 # re-create the mock servers so we get clean connects, the previous 334 # backends could be reconnecting still. 335 for my $port (11514, 11515, 11516) { 336 my $srv = mock_server($port); 337 ok(defined $srv, "mock server created"); 338 push(@mocksrvs, $srv); 339 } 340 341 write_modefile('return "noiothread"'); 342 $p_srv->reload(); 343 wait_reload($watcher); 344 345 my $s = IO::Select->new(); 346 for my $msrv (@mocksrvs) { 347 $s->add($msrv); 348 } 349 my @readable = $s->can_read(0.25); 350 # All three backends should become readable with new sockets. 351 is(scalar @readable, 3, "all listeners became readable"); 352 353 my @bepile = (); 354 my $bes = IO::Select->new(); # selector just for the backend sockets. 355 # Each backend should create one socket per worker thread. 356 for my $msrv (@readable) { 357 my @temp = (); 358 for (0 .. 3) { 359 my $be = accept_backend($msrv); 360 # For this set of tests we need to fetch until no data remains in 361 # the socket. 362 $be->blocking(0); 363 $bes->add($be); 364 push(@temp, $be); 365 } 366 for (0 .. 2) { 367 if ($mocksrvs[$_] == $msrv) { 368 $bepile[$_] = \@temp; 369 } 370 } 371 } 372 373 # clients round robin onto different worker threads, so we can test the 374 # key dist on different offsets. 375 my @cli = (); 376 for (0 .. 2) { 377 my $p = $p_srv->new_sock; 378 379 for my $key (0 .. $keycount) { 380 print $p "mg /test/$key\r\n"; 381 @readable = $bes->can_read(0.25); 382 is(scalar @readable, 1, "only one backend became readable"); 383 my $be = shift @readable; 384 # find which listener this be belongs to 385 for my $x (0 .. 2) { 386 for (@{$bepile[$x]}) { 387 if ($_ == $be) { 388 cmp_ok($x, '==', $keymap{$key}, "key routed to correct listener: " . $keymap{$key}); 389 } 390 } 391 } 392 393 is(scalar <$be>, "mg /test/$key\r\n", "got mg passthrough"); 394 print $be "EN\r\n"; 395 is(scalar <$p>, "EN\r\n", "miss received"); 396 } 397 398 # hold onto the sockets just in case. 399 push(@cli, $p); 400 } 401 402 @readable = $s->can_read(0.25); 403 is(scalar @readable, 0, "no listeners should be active pre-reload"); 404 $p_srv->reload(); 405 wait_reload($watcher); 406 @readable = $s->can_read(0.25); 407 is(scalar @readable, 0, "no listeners should be active post-reload"); 408 409 note "testing batch workload"; 410 411 my $batch = ''; 412 my $batch_size = 50; 413 for (0 .. $batch_size) { 414 $batch .= "ms /test/$_ 5\r\nhello\r\n"; 415 } 416 print $ps $batch; 417 418 my $remain = $batch_size; 419 # Not using the test hardness for the readback to cut spam/time. 420 while ($remain > 0) { 421 my @ready = $bes->can_read(); 422 for my $be (@ready) { 423 while (1) { 424 my $be1 = $be->getline; 425 my $be2 = $be->getline; 426 if ($be1 && $be2) { 427 print $be "HD\r\n"; 428 } else { 429 last; 430 } 431 #diag "read " . $remain; 432 $remain--; 433 } 434 } 435 } 436 437 is($remain, -1, "completed batch workload to backends"); 438 439 for (0 .. $batch_size) { 440 my $res = <$ps>; 441 if ($res ne "HD\r\n") { 442 is($res, "HD\r\n", "correct result returned to client " . $_); 443 } 444 } 445 446 note "done testing batch limit"; 447 448 check_version($ps); 449} 450 451# TODO: 452# remove backends 453# do dead sockets close? 454# adding backends with the same label don't create more connections 455# total backend counters 456# change top level routes mid-request 457# - send the request to backend 458# - issue and wait for reload 459# - read from backend and respond, should use the original code still. 460# - could also read from backend and then do reload/etc. 461 462done_testing(); 463 464END { 465 unlink $modefile; 466} 467