diff --git a/proxy.h b/proxy.h index 752d249ba1..84a8a9f2e6 100644 --- a/proxy.h +++ b/proxy.h @@ -803,6 +803,7 @@ int mcplib_request_flag_set(lua_State *L); int mcplib_request_flag_replace(lua_State *L); int mcplib_request_flag_del(lua_State *L); int mcplib_request_gc(lua_State *L); +int mcplib_request_match_res(lua_State *L); void mcp_request_cleanup(LIBEVENT_THREAD *t, mcp_request_t *rq); // response interface diff --git a/proxy_lua.c b/proxy_lua.c index f25f783c16..d0340f146b 100644 --- a/proxy_lua.c +++ b/proxy_lua.c @@ -1727,6 +1727,7 @@ int proxy_register_libs(void *ctx, LIBEVENT_THREAD *t, void *state) { {"flag_set", mcplib_request_flag_set}, {"flag_replace", mcplib_request_flag_replace}, {"flag_del", mcplib_request_flag_del}, + {"match_res", mcplib_request_match_res}, {"__tostring", NULL}, {"__gc", mcplib_request_gc}, {NULL, NULL} diff --git a/proxy_request.c b/proxy_request.c index 7856689e94..4fb30a05b0 100644 --- a/proxy_request.c +++ b/proxy_request.c @@ -1000,6 +1000,84 @@ int mcplib_request_flag_del(lua_State *L) { return 1; } +// local match, token = req:match_res(res) +// checks if req has `k` or `O`. If so, checks response for `K` or `O` +// returns true, nil if matches +// returns false, res token if not match. +// +int mcplib_request_match_res(lua_State *L) { + mcp_request_t *rq = luaL_checkudata(L, 1, "mcp.request"); + mcp_resp_t *rs = luaL_checkudata(L, 2, "mcp.response"); + + const char *opaque_token = NULL; + size_t opaque_len = 0; + + // requests all have keys. check for an opaque. + mcp_request_find_flag_token(rq, 'O', &opaque_token, &opaque_len); + + // scan the response line for tokens, since we don't have a reciprocal API + // yet. When we do this code will be replaced with a function call like + // the above. + const char *p = rs->resp.rline; + // TODO: Think this is an off-by-one in mcmc. + const char *e = p + rs->resp.rlen - 1; + if (!p) { + // happens if the result line is blank (ie; 'HD\r\n') + lua_pushboolean(L, 0); + lua_pushnil(L); + return 2; + } + + int matched = 0; + while (p != e) { + if (*p == ' ') { + p++; + } else if (*p == 'k' || *p == 'O') { + const char *rq_token = NULL; + int rq_len = 0; + if (*p == 'k') { + rq_token = MCP_PARSER_KEY(rq->pr); + rq_len = rq->pr.klen; + } else if (*p == 'O') { + rq_token = opaque_token; + rq_len = opaque_len; + } + if (rq_token == NULL) { + lua_pushboolean(L, 0); + lua_pushnil(L); + return 2; + } + + p++; // skip flag and start comparing token + const char *rs_token = p; + + // find end of token + while (p != e && !isspace(*p)) { + p++; + } + + int rs_len = p - rs_token; + if (rq_len != rs_len || memcmp(rq_token, rs_token, rs_len) != 0) { + // FAIL, keys aren't the same length or don't match. + lua_pushboolean(L, 0); + lua_pushlstring(L, rs_token, rs_len); + return 2; + } else { + matched = 1; + } + } else { + // skip token + while (p != e && *p != ' ') { + p++; + } + } + } + + lua_pushboolean(L, matched); + lua_pushnil(L); + return 2; +} + void mcp_request_cleanup(LIBEVENT_THREAD *t, mcp_request_t *rq) { // During nread c->item is the malloc'ed buffer. not yet put into // rq->buf - this gets freed because we've also set c->item_malloced if diff --git a/t/lib/MemcachedTest.pm b/t/lib/MemcachedTest.pm index 5b8bec9601..ffbc51733d 100644 --- a/t/lib/MemcachedTest.pm +++ b/t/lib/MemcachedTest.pm @@ -661,8 +661,11 @@ sub be_recv_c { my $l = $self->_be_list($list); my $cmd = $self->{_cmd}; + my @cmds = split(/(?<=\r\n)/, $cmd); for my $be (@$l) { - Test::More::is(scalar <$be>, $cmd, $detail); + for my $c (@cmds) { + Test::More::is(scalar <$be>, $c, $detail); + } } } diff --git a/t/proxymatch.lua b/t/proxymatch.lua new file mode 100644 index 0000000000..88e41fb465 --- /dev/null +++ b/t/proxymatch.lua @@ -0,0 +1,32 @@ +function mcp_config_pools() + mcp.backend_connect_timeout(60) + mcp.backend_read_timeout(60) + mcp.backend_retry_timeout(60) + local b1 = mcp.backend('b1', '127.0.0.1', 12171) + return mcp.pool({b1}) +end + +function mcp_config_routes(p) + local fg = mcp.funcgen_new() + local h = fg:new_handle(p) + fg:ready({ + n = "match", f = function(rctx) + return function(r) + local res = rctx:enqueue_and_wait(r, h) + local match, token = r:match_res(res) + if match then + mcp.log("match succeeded") + else + if token then + mcp.log("match failed: " .. token) + else + mcp.log("match failed: no token") + end + end + return res + end + end + }) + mcp.attach(mcp.CMD_MG, fg) + mcp.attach(mcp.CMD_MS, fg) +end diff --git a/t/proxymatch.t b/t/proxymatch.t new file mode 100644 index 0000000000..b4578e78d1 --- /dev/null +++ b/t/proxymatch.t @@ -0,0 +1,250 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); +use lib "$Bin/lib"; +use Carp qw(croak); +use MemcachedTest; +use IO::Socket qw(AF_INET SOCK_STREAM); +use IO::Select; +use Data::Dumper qw/Dumper/; + +if (!supports_proxy()) { + plan skip_all => 'proxy not enabled'; + exit 0; +} + +my $t = Memcached::ProxyTest->new(servers => [12171]); + +my $p_srv = new_memcached('-o proxy_config=./t/proxymatch.lua -t 1'); +my $ps = $p_srv->sock; +$ps->autoflush(1); + +$t->set_c($ps); +$t->accept_backends(); + +my $w = $p_srv->new_sock; +print $w "watch proxyuser proxyevents\n"; +is(<$w>, "OK\r\n", "watcher enabled"); + +subtest 'ms no tokens' => sub { + my $cmd = "ms foo 2\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +# successful tests. +subtest 'ms with k' => sub { + my $cmd = "ms foo 2 k\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kfoo\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +subtest 'ms with O' => sub { + my $cmd = "ms foo 2 k O1234\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kfoo O1234\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +subtest 'ms with k and O' => sub { + my $cmd = "ms foo 2 k O4321\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kfoo O4321\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +subtest 'ms with O and k' => sub { + my $cmd = "ms foo 2 O9876 k\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O9876 kfoo\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +# ensure the parser works with unrelated tokens at the beginning/end of the string +subtest 'ms with O and k and c' => sub { + my $cmd = "ms foo 2 T99 O9876 k c\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O9876 kfoo c2\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +# test some failures. +subtest 'ms with empty k' => sub { + my $cmd = "ms foo 2 k\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD k\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with empty O' => sub { + my $cmd = "ms foo 2 O\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with empty O c' => sub { + my $cmd = "ms foo 2 O c\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O c2\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with wrong k' => sub { + my $cmd = "ms foo 2 k\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kbar\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with wrong len k' => sub { + my $cmd = "ms foo 2 k\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kfoob\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with wrong O' => sub { + my $cmd = "ms foo 2 O1234\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O4321\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with wrong len O' => sub { + my $cmd = "ms foo 2 O1234\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD O43210\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with right k wrong O' => sub { + my $cmd = "ms foo 2 k O1234\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kfoo O5678\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'ms with right O wrong k' => sub { + my $cmd = "ms foo 2 k O1234\r\nok\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD kbar O1234\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +# don't need to test mg as hard since the match code is the same. just ensure +# the rline handling is correct for both code paths. + +# mg successful tests +subtest 'mg with k' => sub { + my $cmd = "mg foo t k\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t99 kfoo\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +subtest 'mg with O' => sub { + my $cmd = "mg foo t O1234\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t99 O1234\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +subtest 'mg with O and k' => sub { + my $cmd = "mg foo t O1234 k c\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t98 O1234 kfoo c2\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match succeeded/); + $t->clear(); +}; + +# mg failures +subtest 'mg with wrong k' => sub { + my $cmd = "mg foo t k\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t97 kbar\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'mg with wrong O' => sub { + my $cmd = "mg foo t O1234 c\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t97 O4321 c2\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +subtest 'mg no matchable tokens' => sub { + my $cmd = "mg foo t c\r\n"; + $t->c_send($cmd); + $t->be_recv_c(0); + $t->be_send(0, "HD t97 c2\r\n"); + $t->c_recv_be(); + like(<$w>, qr/match failed: /); + $t->clear(); +}; + +done_testing();