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