Skip to content

Commit 28f4abf

Browse files
committed
core: fix pipelined set failures on pending reads
- IO objects are enqueued per-worker-thread since a refactor in ec1fb56 - If a set payload is being read off the network but the read gets EAGAIN, _and_ there are already pending IO objects on the connection, the worker thread will execute the pending IOs - The worker resume code did not check for conn_nread state and would resume the connection's state machine if those IO's completed before the conn_nread state completes. - The connection state is now corrupt and will both leak memory and throw parsing errors. This is a one-line fix to avoid resuming the state machine if we were in conn_nread state, as well as avoiding finalizing a connection from the conn_closing state if there are pending suspended responses.
1 parent a5b3ce9 commit 28f4abf

3 files changed

Lines changed: 110 additions & 4 deletions

File tree

memcached.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,8 @@ void conn_worker_readd(conn *c) {
551551
// any recursion here.
552552
event_active(&c->event, 0, 0);
553553
break;
554+
case conn_nread:
555+
// ran IO queue while waiting for set payload.
554556
case conn_write:
555557
case conn_mwrite:
556558
case conn_read:
@@ -3319,10 +3321,12 @@ static void drive_machine(conn *c) {
33193321
break;
33203322

33213323
case conn_closing:
3322-
if (IS_UDP(c->transport))
3323-
conn_cleanup(c);
3324-
else
3325-
conn_close(c);
3324+
if (!c->resps_suspended) {
3325+
if (IS_UDP(c->transport))
3326+
conn_cleanup(c);
3327+
else
3328+
conn_close(c);
3329+
}
33263330
stop = true;
33273331
break;
33283332

t/proxyms.lua

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
function mcp_config_pools()
2+
mcp.backend_read_timeout(30)
3+
local b1 = mcp.backend('b1', '127.0.0.1', 12174)
4+
return mcp.pool({b1})
5+
end
6+
7+
function mcp_config_routes(p)
8+
local fgen = mcp.funcgen_new()
9+
local handle = fgen:new_handle(p)
10+
11+
fgen:ready({ f = function(rctx)
12+
return function(r)
13+
return rctx:enqueue_and_wait(r, handle)
14+
end
15+
end})
16+
17+
mcp.attach(mcp.CMD_ANY_STORAGE, fgen)
18+
end

t/proxyms.t

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env perl
2+
3+
use strict;
4+
use warnings;
5+
use Test::More;
6+
use FindBin qw($Bin);
7+
use lib "$Bin/lib";
8+
use Carp qw(croak);
9+
use MemcachedTest;
10+
use IO::Socket qw(AF_INET SOCK_STREAM);
11+
use IO::Select;
12+
use Data::Dumper qw/Dumper/;
13+
14+
if (!supports_proxy()) {
15+
plan skip_all => 'proxy not enabled';
16+
exit 0;
17+
}
18+
19+
my $p_srv = new_memcached('-o proxy_config=./t/proxyms.lua -t 1');
20+
my $ps = $p_srv->sock;
21+
$ps->autoflush(1);
22+
23+
my $w = $p_srv->new_sock;
24+
print $w "watch proxyevents\r\n";
25+
is(<$w>, "OK\r\n");
26+
27+
# Slow string generator.
28+
sub rand_string {
29+
my $len = shift;
30+
my $val = '';
31+
my @chars = ("A".."Z");
32+
for (1 .. $len) {
33+
$val .= $chars[rand @chars];
34+
}
35+
return $val;
36+
}
37+
38+
my $req_count = 3;
39+
my @cmds = ();
40+
for (1 .. $req_count) {
41+
my $key = rand_string(100);
42+
my $cmd = "ms $key 203 c T10 O$_\r\n";
43+
my $val = rand_string(203) . "\r\n";
44+
push(@cmds, $cmd, $val);
45+
}
46+
my $rendered = join('', @cmds);
47+
# Add in one more command with a partial payload
48+
$rendered .= "ms " . rand_string(100) . " 203 c T10 O999\r\n";
49+
$rendered .= rand_string(103);
50+
my $payload_final = rand_string(100) . "\r\n";
51+
52+
# Generate N set requests that fit into one network read (< 16kb)
53+
# Add in a third which fails partway through a set payload
54+
# With the pause, the first N requests will get processed, dispatched to the
55+
# backend, and completed instantly since the backend is down.
56+
# Then we send the rest of the payload. With the bug we'll end up with an out
57+
# of sync state machine and different errors.
58+
subtest 'batch ms with split requests' => sub {
59+
syswrite($ps, $rendered, length($rendered));
60+
# Ensure memcached picks up the initial partial buffer.
61+
sleep 0.5;
62+
syswrite($ps, $payload_final, length($payload_final));
63+
for my $n (1 .. $req_count+1) {
64+
my $resline = scalar <$ps>;
65+
is($resline, "SERVER_ERROR backend failure\r\n", "expected error");
66+
}
67+
};
68+
69+
# With the bug, we resume the conn state machine and clobber the pending
70+
# request that was waiting for a payload. Leaks the actvie request.
71+
subtest 'batch ms with closed client' => sub {
72+
# Get a new client so we can close it.
73+
my $d = $p_srv->new_sock;
74+
$d->autoflush(1);
75+
syswrite($d, $rendered, length($rendered));
76+
# Wait for IO execution
77+
sleep 0.5;
78+
$d->close();
79+
80+
my $stats = mem_stats($ps);
81+
is($stats->{proxy_req_active}, 0, "no leaked requests");
82+
};
83+
84+
done_testing();

0 commit comments

Comments
 (0)