t/proxyfuncgen.lua LUA 824 lines View on github.com → Search inside
1-- New style request factories and backend request handling.2--3-- First, this API adds a "request func generation" step when a new request4-- starts: if there is not already a cached function to use, call the5-- "generator" function, then use the response to run the request. This generated6-- function is reused until the parent generator is swapped out during reload.7-- This allows the user to pre-allocate and pre-calculate objects and data,8-- offering both safety and performance.9-- Future API revisions (such as stats) will rely on this generation step to10-- be more user friendly while retaining performance.11--12-- For backend IO's this unifies what was once two API's:13--  - result = pool(request): the non-async API14--  - table = mcp.await(etc)15--16-- It is now a single system governeed by a request context object (rctx).17-- This new system allows queueing a nearly arbitrary set of requests,18-- "blocking" a client on any individual response, and using callbacks to19-- make decisions on if a response is "good", to resume processing early, or20-- post-process once all responses are received.21--22-- The queueing system is now recursive: a fgen can new_handle() another fgen.23-- Meaning configurations can be assembled as call graphs. IE: If you have a24-- route function A and want to "shadow" some of its requests onto route25-- function B, instead of making A more complex you can create a third26-- function C which splits the traffic.27--28-- API docs: https://github.com/memcached/memcached/wiki/Proxy2930verbose = true31-- global for an error handling test32failgen_armed = false33failgenret_armed = false3435function say(...)36    if verbose then37        print(...)38    end39end4041function mcp_config_pools()42    local srv = mcp.backend4344    local b1 = srv('b1', '127.0.0.1', 12011)45    local b2 = srv('b2', '127.0.0.1', 12012)46    local b3 = srv('b3', '127.0.0.1', 12013)47    local b4 = srv('b4', '127.0.0.1', 12014)48    local b1z = mcp.pool({b1})49    local b2z = mcp.pool({b2})50    local b3z = mcp.pool({b3})51    local b4z = mcp.pool({b4})5253    local blog = srv({54        label = "blog",55        host = "127.0.0.1",56        port = "12015",57        log = {58            rate = 5,59            errors = true,60            deadline = 250,61            tag = "fastlog"62        }63    })64    blogz = mcp.pool({blog})6566    local p = {p = {b1z, b2z, b3z}, b = b4z, pl = blogz}6768    --return mcp.pool(b1z, { iothread = false })69    return p70end7172-- many of these factories have the same basic init pattern, so we can save73-- some code.74function new_basic_factory(arg, func)75    local fgen = mcp.funcgen_new()76    local o = { t = {}, c = 0 }7778    -- some of them have a wait, some don't.79    -- here would be a good place to do bounds checking on arguments in80    -- similar functions.81    o.wait = arg.wait82    o.timeout = arg.timeout83    o.mode = arg.mode84    for _, v in pairs(arg.list) do85        table.insert(o.t, fgen:new_handle(v))86        o.c = o.c + 187    end8889    fgen:ready({ f = func, a = o, n = arg.name})90    return fgen91end9293function new_prefix_factory(arg)94    local fgen = mcp.funcgen_new()95    local o = {}96    o.pattern = arg.pattern97    o.default = fgen:new_handle(arg.default)9899    o.map = {}100    -- get handler ids for each sub-route value101    -- convert the map.102    for k, v in pairs(arg.list) do103        o.map[k] = fgen:new_handle(v)104    end105106    fgen:ready({ f = prefix_factory_gen, a = o, n = arg.name })107    return fgen108end109110function prefix_factory_gen(rctx, arg)111    local p = arg.pattern112    local map = arg.map113    local d = arg.default114115    say("generating a prefix factory function")116117    return function(r)118        local key = r:key()119120        local handle = map[string.match(key, p)]121        if handle == nil then122            return rctx:enqueue_and_wait(r, d)123        end124        return rctx:enqueue_and_wait(r, handle)125    end126end127128function new_direct_factory(arg)129    local fgen = mcp.funcgen_new()130    local h = fgen:new_handle(arg.p)131    fgen:ready({ f = direct_factory_gen, a = h, n = arg.name })132    return fgen133end134135function direct_factory_gen(rctx, h)136    say("generating direct factory function")137138    return function(r)139        say("waiting on a single pool")140        return rctx:enqueue_and_wait(r, h)141    end142end143144function new_locality_factory(arg)145    local fgen = mcp.funcgen_new()146    local h = fgen:new_handle(arg.p)147    fgen:ready({ f = locality_factory_gen, a = h, n = arg.name })148    return fgen149end150151-- factory for proving slots have unique environmental memory.152-- we need to wait on a backend to allow the test to pipeline N requests in153-- parallel, to prove that each parallel slot has a unique lua environment.154function locality_factory_gen(rctx, h)155    say("generating locality factory function")156    local x = 0157158    return function(r)159        x = x + 1160        say("returning from locality: " .. x)161        local res = rctx:enqueue_and_wait(r, h)162        return "HD t" .. x .. "\r\n"163    end164end165166-- waits for only the _first_ queued handle to return.167-- ie; position 1 in the table.168-- we do a numeric for loop in the returned function to avoid allocations done169-- by a call to pairs()170function first_factory_gen(rctx, arg)171    say("generating first factory function")172    local t = arg.t173    local count = arg.c174175    return function(r)176        say("waiting on first of " .. count .. " pools")177        for x=1, count do178            rctx:enqueue(r, t[x])179        end180181        return rctx:wait_handle(t[1])182    end183end184185-- wait on x out of y186function partial_factory_gen(rctx, arg)187    say("generating partial factory function")188    local t = arg.t189    local count = arg.c190    local wait = arg.wait191192    return function(r)193        say("waiting on first " .. wait .. " out of " .. count)194        for x=1, count do195            rctx:enqueue(r, t[x])196        end197198        local done = rctx:wait_cond(wait)199        for x=1, count do200            -- :good will only return the result object if the handle's201            -- response was considered "good"202            local res = rctx:res_good(t[x])203            if res ~= nil then204                say("found a result")205                return res206            end207            -- TODO: tally up responses and send summary for test.208        end209        say("found nothing")210        -- didn't return anything good, so return one at random.211        for x=1, count do212            local res = rctx:res_any(t[x])213            if res ~= nil then214                return res215            end216        end217    end218end219220-- wait on all pool arguments221function all_factory_gen(rctx, arg)222    say("generating all factory function")223    local t = arg.t224    local count = arg.c225    if arg.wait ~= nil then226        count = arg.wait227    end228    -- should be a minor speedup avoiding the table lookup.229    local mode = mcp.WAIT_ANY230231    return function(r)232        say("all_factory waiting on " .. count)233234        rctx:enqueue(r, t)235        local done = rctx:wait_cond(count, mode)236        -- :any will give us the result object for that handle, regardless237        -- of return code/status.238        local res = rctx:res_any(t[1])239240        -- TODO: tally up the responses and return summary for test.241        return res242    end243end244245-- wait on the first good or N of total246function fastgood_factory_gen(rctx, arg)247    say("generating fastgood factory function")248    local t = arg.t249    local count = arg.c250    local wait = arg.wait251252    local cb = function(res)253        say("running in a callback!")254        if res:hit() then255            say("was a hit!")256            -- return an extra arg telling us to shortcut the wait count257            return mcp.WAIT_GOOD, mcp.WAIT_RESUME258        end259        -- default return code is mcp.WAIT_ANY260    end261262    for _, v in pairs(t) do263        rctx:handle_set_cb(v, cb)264    end265266    return function(r)267        say("first good or wait for N")268269        rctx:enqueue(r, t)270        local done = rctx:wait_cond(wait, mcp.WAIT_GOOD)271        say("fastgood done:", done)272273        if done == 1 then274            -- if we just got one "good", we're probably happy.275            for x=1, count do276                -- loop to find the good handle.277                local res = rctx:res_good(t[x])278                if res ~= nil then279                    return res280                end281            end282        else283            -- else we had to wait and now need to decide if it was a miss or284            -- network error.285            -- but for this test we'll just return the first result.286            for x=1, count do287                local res = rctx:res_any(t[x])288                if res ~= nil then289                    return res290                end291            end292        end293    end294end295296-- fastgood implemented using internal fastgood state297function fastgoodint_factory_gen(rctx, arg)298    local t = arg.t299    local count = arg.c300    local wait = arg.wait301302    return function(r)303        rctx:enqueue(r, t)304        say("enqueing fastgood:", wait)305        local done = rctx:wait_cond(wait, mcp.WAIT_FASTGOOD)306        say("fastgoodint done:", done)307308        local final = nil309        for x=1, count do310            local res, mode = rctx:result(t[x])311            if mode == mcp.WAIT_GOOD then312                return res313            elseif res ~= nil then314                final = res315            end316        end317        -- if no good found, return anything.318        return final319    end320end321322function new_blocker_factory(arg)323    local fgen = mcp.funcgen_new()324    local o = { c = 0, t = {} }325    o.b = fgen:new_handle(arg.blocker)326327    for _, v in pairs(arg.list) do328        table.insert(o.t, fgen:new_handle(v))329        o.c = o.c + 1330    end331332    fgen:ready({ f = blocker_factory_gen, a = o, n = arg.name })333    return fgen334end335336-- queue a bunch, but shortcut if a special auxiliary handle fails337function blocker_factory_gen(rctx, arg)338    say("generating blocker factory function")339    local t = arg.t340    local count = arg.c341    local blocker = arg.b342    local was_blocked = false343344    local cb = function(res)345        -- check the response or tokens or anything special to indicate346        -- success.347        -- for this test we just check if it was a hit.348        if res:hit() then349            was_blocked = false350            return mcp.WAIT_GOOD351        else352            was_blocked = true353            return mcp.WAIT_ANY354        end355    end356357    rctx:handle_set_cb(blocker, cb)358359    return function(r)360        say("function blocker test")361362        -- queue up the real queries we wanted to run.363        rctx:enqueue(r, t)364365        -- any wait command will execute all queued queries at once, but here366        -- we only wait for the blocker to complete.367        local bres = rctx:enqueue_and_wait(r, blocker)368369        -- another way of doing this is to ask:370        -- local res = rctx:res_good(blocker)371        -- if a result was returned, the callback had returned WAIT_GOOD372        if was_blocked == false then373            -- our blocker is happy...374            -- wait for the rest of the handles to come in and make a decision375            -- on what to return to the client.376            local done = rctx:wait_cond(count, mcp.WAIT_ANY)377            return rctx:res_any(t[1])378        else379            return "SERVER_ERROR blocked\r\n"380        end381    end382end383384-- log on all callbacks, even if waiting for 1385function logall_factory_gen(rctx, arg)386    say("generating logall factory function")387    local t = arg.t388389    local cb = function(res, req)390        say("received a response, logging...")391        mcp.log("received a response: " .. tostring(res:ok()))392        mcp.log_req(req, res, "even more logs", rctx:cfd())393        return mcp.WAIT_ANY394    end395396    for _, v in pairs(t) do397        rctx:handle_set_cb(v, cb)398    end399400    return function(r)401        rctx:enqueue(r, t)402        return rctx:wait_handle(t[1])403    end404end405406-- log a summary after all callbacks run407function summary_factory_gen(rctx, arg)408    say("generating summary factory function")409    local t = arg.t410    local count = arg.c411412    local todo = 0413    local cb = function(res)414        say("responses TODO: " .. todo)415        todo = todo - 1416        if todo == 0 then417            mcp.log("received all responses")418        end419    end420421    for _, v in pairs(t) do422        rctx:handle_set_cb(v, cb)423    end424425    return function(r)426        -- re-seed the todo value that the callback uses427        todo = count428429        rctx:enqueue(r, t)430        -- we're just waiting for a single response, but we queue all of the431        -- handles. the callback uses data from the shared environment and a432        -- summary is logged.433        return rctx:wait_handle(t[1])434    end435end436437-- testing various waitfor conditions.438function waitfor_factory_gen(rctx, arg)439    say("generating background factory function")440    local t = arg.t441    local count = arg.c442443    return function(r)444        local key = r:key()445        if key == "waitfor/a" then446            rctx:enqueue(r, t)447            rctx:wait_cond(0) -- issue the requests in the background448            return "HD t1\r\n" -- return whatever to the client449        elseif key == "waitfor/b" then450            rctx:enqueue(r, t)451            rctx:wait_cond(0) -- issue requests and resume452            -- now go back into wait mode, but we've already dispatched453            local done = rctx:wait_cond(2)454            if done ~= 2 then455                return "SERVER_ERROR invalid wait"456            end457            -- TODO: bonus points, count the goods or check that everyone's t458            -- flag is right.459            for x=1, count do460                local res = rctx:res_good(x)461                if res ~= nil then462                    return res463                end464                return "SERVER_ERROR no good response"465            end466        elseif key == "waitfor/c" then467            rctx:enqueue(r, t[1])468            rctx:wait_cond(0) -- issue the first queued request469            -- queue two more470            rctx:enqueue(r, t[2])471            rctx:enqueue(r, t[3])472            -- wait explicitly for the first queued one.473            return rctx:wait_handle(t[1])474        elseif key == "waitfor/d" then475            -- queue two then wait on each individually476            rctx:enqueue(r, t[1])477            rctx:enqueue(r, t[2])478            rctx:wait_handle(t[1])479            return rctx:wait_handle(t[2])480        end481    end482end483484-- try "primary zone" and then fail over to secondary zones.485-- using simplified code that just treats the first pool as the primary zone.486function failover_factory_gen(rctx, arg)487    say("generating failover factory function")488    local t = {}489    local count = arg.c490    local first = arg.t[1]491492    for x=2, count do493        table.insert(t, arg.t[x])494    end495496    return function(r)497        -- first try local498        local fres = rctx:enqueue_and_wait(r, first)499500        if fres == nil or fres:hit() == false then501            -- failed to get a local hit, queue all "far" zones.502            rctx:enqueue(r, t)503            -- wait for one.504            local done = rctx:wait_cond(1, mcp.WAIT_GOOD)505            -- find the good from the second set.506            for x=1, count-1 do507                local res = rctx:res_good(t[x])508                if res ~= nil then509                    say("found a result")510                    return res511                end512            end513            -- got nothing from second set, just return anything.514            return rctx:res_any(first)515        else516            return fres517        end518    end519end520521function new_msg_factory(msg, name)522    local fgen = mcp.funcgen_new()523    fgen:ready({ n = name, f = function(rctx)524        return function(r)525            return msg526        end527    end})528    return fgen529end530531function new_error_factory(func, name)532    local fgen = mcp.funcgen_new()533    fgen:ready({ f = func, n = name })534    return fgen535end536537function errors_factory_gen(rctx)538    say("generating errors factory")539540    return function(r)541        local key = r:key()542        -- failure scenarios that require a top-level request context543        if key == "errors/reterror" then544            error("test error")545        elseif key == "errors/retnil" then546            return nil547        elseif key == "errors/retint" then548            return 5549        elseif key == "errors/retnone" then550            return551        end552    end553end554555function suberrors_factory_gen(rctx)556    say("generating suberrors factory function")557558    return function(r)559        local key = r:key()560        if key == "suberrors/error" then561            error("test error")562        elseif key == "suberrors/nil" then563            return nil564        elseif key == "suberrors/int" then565            return 5566        elseif key == "suberrors/none" then567            return568        elseif key == "suberrors/resume" then569            rctx:sleep(0.25)570            error("error after resuming rctx")571        elseif key == "suberrors/string" then572            -- non error but immediate return scenario.573            return "SERVER_ERROR suberror/string\r\n"574        end575576    end577end578579function new_split_factory(arg)580    local fgen = mcp.funcgen_new()581    local o = {}582    o.a = fgen:new_handle(arg.a)583    o.b = fgen:new_handle(arg.b)584    fgen:ready({ f = split_factory_gen, a = o, n = name })585    return fgen586end587588-- example of a factory that takes two other factories and copies traffic589-- across them.590-- If an additional API's for hashing to numerics are added, keys can be591-- hashed to allow "1/n" of keys to copy to one of the splits. This allows592-- shadowing traffic to new/experimental pools, slow-warming traffic, etc.593function split_factory_gen(rctx, arg)594    say("generating split factory function")595    local a = arg.a596    local b = arg.b597598    return function(r)599        say("splitting traffic")600        -- b is the split path.601        rctx:enqueue(r, b)602603        -- a is the main path. so we only explicitly wait on and return a.604        return rctx:enqueue_and_wait(r, a)605    end606end607608-- test handling of failure to generate a function slot609function failgen_factory_gen(rctx)610    if failgen_armed then611        say("throwing failgen error")612        error("failgen")613    end614    say("arming failgen")615    failgen_armed = true616617    return function(r)618        return "NF\r\n"619    end620end621622function failgenret_factory_gen(rctx)623    if failgenret_armed then624        return nil625    end626    failgenret_armed = true627628    return function(r)629        return "NF\r\n"630    end631end632633function badreturn_gen(rctx)634    -- returning a userdata that isn't the correct kind of userdata.635    -- shouldn't crash the daemon!636    return function(r)637        return rctx638    end639end640641-- TODO: should really make the wait time ignored if timeout is nil? throws an642-- error but means more lua to deal with optional waits...643function bestres_factory_gen(rctx, arg)644    local handles = arg.t645    local timeout = nil646    if arg.timeout then647        timeout = arg.timeout648    end649    return function(r)650        rctx:enqueue(r, handles)651        if timeout then652            rctx:wait_cond(#handles, mcp.WAIT_ANY, timeout)653        else654            rctx:wait_cond(#handles)655        end656        local res, tag = rctx:best_result(handles)657        return res658    end659end660661function worstres_factory_gen(rctx, arg)662    local handles = arg.t663    return function(r)664        rctx:enqueue(r, handles)665        rctx:wait_cond(#handles)666        local res, tag = rctx:worst_result(handles)667        return res668    end669end670671function timeout_factory_gen(rctx, arg)672    local handles = arg.t673    local wait = #handles674    local timeout = nil675    local mode = mcp.WAIT_GOOD676    if arg.wait then677        wait = arg.wait678    end679    if arg.timeout then680        timeout = arg.timeout681    end682    if arg.mode then683        mode = arg.mode684    end685    return function(r)686        rctx:enqueue(r, handles)687        if timeout then688            rctx:wait_cond(wait, mode, timeout)689        else690            rctx:wait_cond(wait, mode)691        end692        local res, tag = rctx:best_result(handles)693        return res694    end695end696697-- TODO: this might be supported only in a later update.698-- new queue after parent return699-- - do an immediate return + cb queue, queue from that callback700-- - should still work but requires worker lua vm701-- requires removing the need of having an active client socket object to702-- queue new requests for processing.703function postreturn_factory(rctx, arg)704705end706707-- TODO: demonstrate a split call graph708-- ie; an all split into two single709710function mcp_config_routes(p)711    local b_pool = p.b712    local pl = p.pl713    p = p.p714    local single = new_direct_factory({ p = p[1], name = "single" })715    -- use the typically unused backend.716    local singletwo = new_direct_factory({ p = b_pool, name = "singletwo" })717718    local first = new_basic_factory({ list = p, name = "first" }, first_factory_gen)719    local partial = new_basic_factory({ list = p, wait = 2, name = "partial" }, partial_factory_gen)720    local all = new_basic_factory({ list = p, name = "all" }, all_factory_gen)721    local fastgood = new_basic_factory({ list = p, wait = 2, name = "fastgood" }, fastgood_factory_gen)722    local fastgoodint = new_basic_factory({ list = p, wait = 2, name = "fastgoodint" }, fastgoodint_factory_gen)723    local blocker = new_blocker_factory({ blocker = b_pool, list = p, name = "blocker" })724    local logall = new_basic_factory({ list = p, name = "logall" }, logall_factory_gen)725    local fastlog = new_direct_factory({ p = pl, name = "fastlog" })726    local subfastlog = new_direct_factory({ p = fastlog, name = "subfastlog" })727    local summary = new_basic_factory({ list = p, name = "summary" }, summary_factory_gen)728    local waitfor = new_basic_factory({ list = p, name = "waitfor" }, waitfor_factory_gen)729    local failover = new_basic_factory({ list = p, name = "failover" }, failover_factory_gen)730    local locality = new_locality_factory({ p = p[1], name = "locality" })731732    local errors = new_error_factory(errors_factory_gen, "errors")733    local suberrors = new_error_factory(suberrors_factory_gen, "suberrors")734    local suberr_wrap = new_direct_factory({ p = suberrors, name = "suberrwrap" })735    local badreturn = new_error_factory(badreturn_gen, "badreturn")736737    local bestres = new_basic_factory({ list = p, name = "bestres" }, bestres_factory_gen)738    local bestrestime = new_basic_factory({ list = p, timeout = 0.5, name = "bestres" }, bestres_factory_gen)739    local worstres = new_basic_factory({ list = p, name = "worstres" }, worstres_factory_gen)740741    -- for testing traffic splitting.742    local split = new_split_factory({ a = single, b = singletwo, name = "split" })743    local splitfailover = new_split_factory({ a = failover, b = singletwo, name = "splitfailover" })744745    -- test timeout via subrctx's that themselves timeout746    local timesubone = new_basic_factory({ list = { p[1] }, timeout = 0.25, name = "timesubone" }, timeout_factory_gen)747    local timesubtwo = new_basic_factory({ list = { p[2] }, timeout = 0.25, name = "timesubone" }, timeout_factory_gen)748    local timesubthr = new_basic_factory({ list = { p[3] }, timeout = 0.25, name = "timesubone" }, timeout_factory_gen)749    local timetop = new_basic_factory({ list = { timesubone, timesubtwo, timesubthr }, wait = 1, name = "timetop" }, timeout_factory_gen)750    local timefgtop = new_basic_factory({ list = { timesubone, timesubtwo, timesubthr }, wait = 2, mode = mcp.WAIT_FASTGOOD, name = "timefgtop" }, timeout_factory_gen)751752    -- complex stacking:753    -- - parent754    -- - sub with direct child755    -- - child that splits requests, waits756    -- - one sub-child with 3 subrctx children, each with a pool757    -- - the sub-mid child throws fatal error after enqueueing requests.758    -- bug was pending request refcount for final child increasing before759    -- dispatching instead of after dispatching.760    local complex_childa = new_direct_factory({ p = p[1], name = "cmpchilda" })761    local complex_childb = new_direct_factory({ p = p[2], name = "cmpchildb" })762    local complex_childc = new_direct_factory({ p = p[3], name = "cmpchildc" })763    local complex_msg = new_msg_factory("SERVER_ERROR toast\r\n", "cmpmsg")764    local complex_fastgoodint = new_basic_factory({ list = { complex_childa, complex_childb, complex_childc }, name = "cmpfastgoodint" }, fastgoodint_factory_gen)765    local complex_mid = new_basic_factory({ list = { complex_childa, complex_childb, complex_fastgoodint }, wait = 0, name = "cmpmid" }, all_factory_gen)766    local complex_top = new_direct_factory({ p = complex_mid, name = "complex" })767768    local map = {769        ["single"] = single,770        ["first"] = first,771        ["partial"] = partial,772        ["all"] = all,773        ["fastgood"] = fastgood,774        ["fastgoodint"] = fastgoodint,775        ["blocker"] = blocker,776        ["logall"] = logall,777        ["fastlog"] = fastlog,778        ["subfastlog"] = subfastlog,779        ["summary"] = summary,780        ["waitfor"] = waitfor,781        ["failover"] = failover,782        ["suberrors"] = suberr_wrap,783        ["errors"] = errors,784        ["split"] = split,785        ["splitfailover"] = splitfailover,786        ["locality"] = locality,787        ["badreturn"] = badreturn,788        ["bestres"] = bestres,789        ["bestrestime"] = bestrestime,790        ["worstres"] = worstres,791        ["timetop"] = timetop,792        ["timefgtop"] = timefgtop,793        ["complex"] = complex_top,794    }795796    local parg = {797        default = single,798        list = map,799        pattern = "^/(%a+)/"800    }801802    local failgen = new_error_factory(failgen_factory_gen, "failgen")803    local failgenret = new_error_factory(failgenret_factory_gen, "failgenret")804805    local mapfail = {806        ["failgen"] = failgen,807        ["failgenret"] = failgenret,808    }809    local farg = {810        default = single,811        list = mapfail,812        pattern = "^(%a+)/",813        name = "prefixfail"814    }815816    local pfx = mcp.router_new({ map = map })817    local pfxfail = new_prefix_factory(farg)818819    mcp.attach(mcp.CMD_ANY_STORAGE, pfx)820    -- TODO: might need to move this fail stuff to another test file.821    mcp.attach(mcp.CMD_MS, pfxfail)822    mcp.attach(mcp.CMD_MD, pfxfail)823end

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings