feat(apisix): add Cloudron package
- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local core = require("apisix.core")
|
||||
local sdk = require("apisix.stream.xrpc.sdk")
|
||||
local xrpc_socket = require("resty.apisix.stream.xrpc.socket")
|
||||
local math_random = math.random
|
||||
local ngx = ngx
|
||||
local OK = ngx.OK
|
||||
local str_format = string.format
|
||||
local DECLINED = ngx.DECLINED
|
||||
local DONE = ngx.DONE
|
||||
local bit = require("bit")
|
||||
local ffi = require("ffi")
|
||||
local ffi_str = ffi.string
|
||||
|
||||
|
||||
-- dubbo protocol spec: https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/tcp/
|
||||
local header_len = 16
|
||||
local _M = {}
|
||||
|
||||
|
||||
function _M.init_downstream(session)
|
||||
session.req_id_seq = 0
|
||||
session.resp_id_seq = 0
|
||||
session.cmd_labels = { session.route.id, "" }
|
||||
return xrpc_socket.downstream.socket()
|
||||
end
|
||||
|
||||
|
||||
local function parse_dubbo_header(header)
|
||||
for i = 1, header_len do
|
||||
local currentByte = header:byte(i)
|
||||
if not currentByte then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local magic_number = str_format("%04x", header:byte(1) * 256 + header:byte(2))
|
||||
local message_flag = header:byte(3)
|
||||
local status = header:byte(4)
|
||||
local request_id = 0
|
||||
for i = 5, 12 do
|
||||
request_id = request_id * 256 + header:byte(i)
|
||||
end
|
||||
|
||||
local byte13Val = header:byte(13) * 256 * 256 * 256
|
||||
local byte14Val = header:byte(14) * 256 * 256
|
||||
local data_length = byte13Val + byte14Val + header:byte(15) * 256 + header:byte(16)
|
||||
|
||||
local is_request = bit.band(bit.rshift(message_flag, 7), 0x01) == 1 and 1 or 0
|
||||
local is_two_way = bit.band(bit.rshift(message_flag, 6), 0x01) == 1 and 1 or 0
|
||||
local is_event = bit.band(bit.rshift(message_flag, 5), 0x01) == 1 and 1 or 0
|
||||
|
||||
return {
|
||||
magic_number = magic_number,
|
||||
message_flag = message_flag,
|
||||
is_request = is_request,
|
||||
is_two_way = is_two_way,
|
||||
is_event = is_event,
|
||||
status = status,
|
||||
request_id = request_id,
|
||||
data_length = data_length
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
local function read_data(sk, is_req)
|
||||
local header_data, err = sk:read(header_len)
|
||||
if not header_data then
|
||||
return nil, err, false
|
||||
end
|
||||
|
||||
local header_str = ffi_str(header_data, header_len)
|
||||
local header_info = parse_dubbo_header(header_str)
|
||||
if not header_info then
|
||||
return nil, "header insufficient", false
|
||||
end
|
||||
|
||||
local is_valid_magic_number = header_info.magic_number == "dabb"
|
||||
if not is_valid_magic_number then
|
||||
return nil, str_format("unknown magic number: \"%s\"", header_info.magic_number), false
|
||||
end
|
||||
|
||||
local body_data, err = sk:read(header_info.data_length)
|
||||
if not body_data then
|
||||
core.log.error("failed to read dubbo request body")
|
||||
return nil, err, false
|
||||
end
|
||||
|
||||
local ctx = ngx.ctx
|
||||
ctx.dubbo_serialization_id = bit.band(header_info.message_flag, 0x1F)
|
||||
|
||||
if is_req then
|
||||
ctx.dubbo_req_body_data = body_data
|
||||
else
|
||||
ctx.dubbo_rsp_body_data = body_data
|
||||
end
|
||||
|
||||
return true, nil, false
|
||||
end
|
||||
|
||||
|
||||
local function read_req(sk)
|
||||
return read_data(sk, true)
|
||||
end
|
||||
|
||||
|
||||
local function read_reply(sk)
|
||||
return read_data(sk, false)
|
||||
end
|
||||
|
||||
|
||||
local function handle_reply(session, sk)
|
||||
local ok, err = read_reply(sk)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local ctx = sdk.get_req_ctx(session, 10)
|
||||
|
||||
return ctx
|
||||
end
|
||||
|
||||
|
||||
function _M.from_downstream(session, downstream)
|
||||
local read_pipeline = false
|
||||
session.req_id_seq = session.req_id_seq + 1
|
||||
local ctx = sdk.get_req_ctx(session, session.req_id_seq)
|
||||
session._downstream_ctx = ctx
|
||||
while true do
|
||||
local ok, err, pipelined = read_req(downstream)
|
||||
if not ok then
|
||||
if err ~= "timeout" and err ~= "closed" then
|
||||
core.log.error("failed to read request: ", err)
|
||||
end
|
||||
|
||||
if read_pipeline and err == "timeout" then
|
||||
break
|
||||
end
|
||||
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
if not pipelined then
|
||||
break
|
||||
end
|
||||
|
||||
if not read_pipeline then
|
||||
read_pipeline = true
|
||||
-- set minimal read timeout to read pipelined data
|
||||
downstream:settimeouts(0, 0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
if read_pipeline then
|
||||
-- set timeout back
|
||||
downstream:settimeouts(0, 0, 0)
|
||||
end
|
||||
|
||||
return OK, ctx
|
||||
end
|
||||
|
||||
|
||||
function _M.connect_upstream(session, ctx)
|
||||
local conf = session.upstream_conf
|
||||
local nodes = conf.nodes
|
||||
if #nodes == 0 then
|
||||
core.log.error("failed to connect: no nodes")
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
local node = nodes[math_random(#nodes)]
|
||||
local sk = sdk.connect_upstream(node, conf)
|
||||
if not sk then
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
core.log.debug("dubbo_connect_upstream end")
|
||||
|
||||
return OK, sk
|
||||
end
|
||||
|
||||
function _M.disconnect_upstream(session, upstream)
|
||||
sdk.disconnect_upstream(upstream, session.upstream_conf)
|
||||
end
|
||||
|
||||
|
||||
function _M.to_upstream(session, ctx, downstream, upstream)
|
||||
local ok, _ = upstream:move(downstream)
|
||||
if not ok then
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
return OK
|
||||
end
|
||||
|
||||
|
||||
function _M.from_upstream(session, downstream, upstream)
|
||||
local ctx,err = handle_reply(session, upstream)
|
||||
if err then
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
local ok, _ = downstream:move(upstream)
|
||||
if not ok then
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
return DONE, ctx
|
||||
end
|
||||
|
||||
|
||||
function _M.log(_, _)
|
||||
end
|
||||
|
||||
|
||||
return _M
|
@@ -0,0 +1,32 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local core = require("apisix.core")
|
||||
|
||||
|
||||
local schema = {
|
||||
type = "object",
|
||||
}
|
||||
|
||||
local _M = {}
|
||||
|
||||
|
||||
function _M.check_schema(conf)
|
||||
return core.schema.check(schema, conf)
|
||||
end
|
||||
|
||||
|
||||
return _M
|
@@ -0,0 +1,222 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local ipairs = ipairs
|
||||
local pairs = pairs
|
||||
|
||||
|
||||
local cmd_to_key_finder = {}
|
||||
--[[
|
||||
-- the data is generated from the script below
|
||||
local redis = require "resty.redis"
|
||||
local red = redis:new()
|
||||
|
||||
local ok, err = red:connect("127.0.0.1", 6379)
|
||||
if not ok then
|
||||
ngx.say("failed to connect: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local res = red:command("info")
|
||||
local map = {}
|
||||
for _, r in ipairs(res) do
|
||||
local first_key = r[4]
|
||||
local last_key = r[5]
|
||||
local step = r[6]
|
||||
local idx = first_key .. ':' .. last_key .. ':' .. step
|
||||
|
||||
if idx ~= "1:1:1" then
|
||||
-- "1:1:1" is the default
|
||||
if map[idx] then
|
||||
table.insert(map[idx], r[1])
|
||||
else
|
||||
map[idx] = {r[1]}
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, r in pairs(map) do
|
||||
table.sort(r)
|
||||
end
|
||||
local dump = require('pl.pretty').dump; dump(map)
|
||||
--]]
|
||||
local key_to_cmd = {
|
||||
["0:0:0"] = {
|
||||
"acl",
|
||||
"asking",
|
||||
"auth",
|
||||
"bgrewriteaof",
|
||||
"bgsave",
|
||||
"blmpop",
|
||||
"bzmpop",
|
||||
"client",
|
||||
"cluster",
|
||||
"command",
|
||||
"config",
|
||||
"dbsize",
|
||||
"debug",
|
||||
"discard",
|
||||
"echo",
|
||||
"eval",
|
||||
"eval_ro",
|
||||
"evalsha",
|
||||
"evalsha_ro",
|
||||
"exec",
|
||||
"failover",
|
||||
"fcall",
|
||||
"fcall_ro",
|
||||
"flushall",
|
||||
"flushdb",
|
||||
"function",
|
||||
"hello",
|
||||
"info",
|
||||
"keys",
|
||||
"lastsave",
|
||||
"latency",
|
||||
"lmpop",
|
||||
"lolwut",
|
||||
"memory",
|
||||
"module",
|
||||
"monitor",
|
||||
"multi",
|
||||
"object",
|
||||
"pfselftest",
|
||||
"ping",
|
||||
"psubscribe",
|
||||
"psync",
|
||||
"publish",
|
||||
"pubsub",
|
||||
"punsubscribe",
|
||||
"quit",
|
||||
"randomkey",
|
||||
"readonly",
|
||||
"readwrite",
|
||||
"replconf",
|
||||
"replicaof",
|
||||
"reset",
|
||||
"role",
|
||||
"save",
|
||||
"scan",
|
||||
"script",
|
||||
"select",
|
||||
"shutdown",
|
||||
"sintercard",
|
||||
"slaveof",
|
||||
"slowlog",
|
||||
"subscribe",
|
||||
"swapdb",
|
||||
"sync",
|
||||
"time",
|
||||
"unsubscribe",
|
||||
"unwatch",
|
||||
"wait",
|
||||
"xgroup",
|
||||
"xinfo",
|
||||
"xread",
|
||||
"xreadgroup",
|
||||
"zdiff",
|
||||
"zinter",
|
||||
"zintercard",
|
||||
"zmpop",
|
||||
"zunion"
|
||||
},
|
||||
["1:-1:1"] = {
|
||||
"del",
|
||||
"exists",
|
||||
"mget",
|
||||
"pfcount",
|
||||
"pfmerge",
|
||||
"sdiff",
|
||||
"sdiffstore",
|
||||
"sinter",
|
||||
"sinterstore",
|
||||
"ssubscribe",
|
||||
"sunion",
|
||||
"sunionstore",
|
||||
"sunsubscribe",
|
||||
"touch",
|
||||
"unlink",
|
||||
"watch"
|
||||
},
|
||||
["1:-1:2"] = {
|
||||
"mset",
|
||||
"msetnx"
|
||||
},
|
||||
["1:-2:1"] = {
|
||||
"blpop",
|
||||
"brpop",
|
||||
"bzpopmax",
|
||||
"bzpopmin"
|
||||
},
|
||||
["1:2:1"] = {
|
||||
"blmove",
|
||||
"brpoplpush",
|
||||
"copy",
|
||||
"geosearchstore",
|
||||
"lcs",
|
||||
"lmove",
|
||||
"rename",
|
||||
"renamenx",
|
||||
"rpoplpush",
|
||||
"smove",
|
||||
"zrangestore"
|
||||
},
|
||||
["2:-1:1"] = {
|
||||
"bitop"
|
||||
},
|
||||
["2:2:1"] = {
|
||||
"pfdebug"
|
||||
},
|
||||
["3:3:1"] = {
|
||||
"migrate"
|
||||
}
|
||||
}
|
||||
local key_finders = {
|
||||
["0:0:0"] = false,
|
||||
["1:-1:1"] = function (idx, narg)
|
||||
return 1 < idx
|
||||
end,
|
||||
["1:-1:2"] = function (idx, narg)
|
||||
return 1 < idx and idx % 2 == 0
|
||||
end,
|
||||
["1:-2:1"] = function (idx, narg)
|
||||
return 1 < idx and idx < narg - 1
|
||||
end,
|
||||
["1:2:1"] = function (idx, narg)
|
||||
return idx == 2 or idx == 3
|
||||
end,
|
||||
["2:-1:1"] = function (idx, narg)
|
||||
return 2 < idx
|
||||
end,
|
||||
["2:2:1"] = function (idx, narg)
|
||||
return idx == 3
|
||||
end,
|
||||
["3:3:1"] = function (idx, narg)
|
||||
return idx == 4
|
||||
end
|
||||
}
|
||||
for k, cmds in pairs(key_to_cmd) do
|
||||
for _, cmd in ipairs(cmds) do
|
||||
cmd_to_key_finder[cmd] = key_finders[k]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
cmd_to_key_finder = cmd_to_key_finder,
|
||||
default_key_finder = function (idx, narg)
|
||||
return idx == 2
|
||||
end,
|
||||
}
|
@@ -0,0 +1,499 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local core = require("apisix.core")
|
||||
local sdk = require("apisix.stream.xrpc.sdk")
|
||||
local commands = require("apisix.stream.xrpc.protocols.redis.commands")
|
||||
local xrpc_socket = require("resty.apisix.stream.xrpc.socket")
|
||||
local ffi = require("ffi")
|
||||
local ffi_str = ffi.string
|
||||
local math_random = math.random
|
||||
local OK = ngx.OK
|
||||
local DECLINED = ngx.DECLINED
|
||||
local DONE = ngx.DONE
|
||||
local sleep = ngx.sleep
|
||||
local str_byte = string.byte
|
||||
local str_fmt = string.format
|
||||
local ipairs = ipairs
|
||||
local tonumber = tonumber
|
||||
|
||||
|
||||
-- this variable is only used to log the redis command line in log_format
|
||||
-- and is not used for filter in the logger phase.
|
||||
core.ctx.register_var("redis_cmd_line", function(ctx)
|
||||
return core.table.concat(ctx.cmd_line, " ")
|
||||
end)
|
||||
|
||||
-- redis protocol spec: https://redis.io/docs/reference/protocol-spec/
|
||||
-- There is no plan to support inline command format
|
||||
local protocol_name = "redis"
|
||||
local _M = {}
|
||||
local MAX_LINE_LEN = 128
|
||||
local MAX_VALUE_LEN = 128
|
||||
local PREFIX_ARR = str_byte("*")
|
||||
local PREFIX_STR = str_byte("$")
|
||||
local PREFIX_STA = str_byte("+")
|
||||
local PREFIX_INT = str_byte(":")
|
||||
local PREFIX_ERR = str_byte("-")
|
||||
|
||||
|
||||
local lrucache = core.lrucache.new({
|
||||
type = "plugin",
|
||||
})
|
||||
|
||||
|
||||
local function create_matcher(conf)
|
||||
local matcher = {}
|
||||
--[[
|
||||
{"delay": 5, "key":"x", "commands":["GET", "MGET"]}
|
||||
{"delay": 5, "commands":["GET"]}
|
||||
=> {
|
||||
get = {keys = {x = {delay = 5}, * = {delay = 5}}}
|
||||
mget = {keys = {x = {delay = 5}}}
|
||||
}
|
||||
]]--
|
||||
for _, rule in ipairs(conf.faults) do
|
||||
for _, cmd in ipairs(rule.commands) do
|
||||
cmd = cmd:lower()
|
||||
local key = rule.key
|
||||
local kf = commands.cmd_to_key_finder[cmd]
|
||||
local key_matcher = matcher[cmd]
|
||||
if not key_matcher then
|
||||
key_matcher = {
|
||||
keys = {}
|
||||
}
|
||||
matcher[cmd] = key_matcher
|
||||
end
|
||||
|
||||
if not key or kf == false then
|
||||
key = "*"
|
||||
end
|
||||
|
||||
if key_matcher.keys[key] then
|
||||
core.log.warn("override existent fault rule of cmd: ", cmd, ", key: ", key)
|
||||
end
|
||||
|
||||
key_matcher.keys[key] = rule
|
||||
end
|
||||
end
|
||||
|
||||
return matcher
|
||||
end
|
||||
|
||||
|
||||
local function get_matcher(conf, ctx)
|
||||
return core.lrucache.plugin_ctx(lrucache, ctx, nil, create_matcher, conf)
|
||||
end
|
||||
|
||||
|
||||
function _M.init_downstream(session)
|
||||
local conf = session.route.protocol.conf
|
||||
if conf and conf.faults then
|
||||
local matcher = get_matcher(conf, session.conn_ctx)
|
||||
session.matcher = matcher
|
||||
end
|
||||
|
||||
session.req_id_seq = 0
|
||||
session.resp_id_seq = 0
|
||||
session.cmd_labels = {session.route.id, ""}
|
||||
return xrpc_socket.downstream.socket()
|
||||
end
|
||||
|
||||
|
||||
local function read_line(sk)
|
||||
local p, err, len = sk:read_line(MAX_LINE_LEN)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
if len < 2 then
|
||||
return nil, "line too short"
|
||||
end
|
||||
|
||||
return p, nil, len
|
||||
end
|
||||
|
||||
|
||||
local function read_len(sk)
|
||||
local p, err, len = read_line(sk)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local s = ffi_str(p + 1, len - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
return nil, str_fmt("invalid len string: \"%s\"", s)
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
local function read_req(session, sk)
|
||||
local narg, err = read_len(sk)
|
||||
if not narg then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local cmd_line = core.tablepool.fetch("xrpc_redis_cmd_line", narg, 0)
|
||||
|
||||
local n, err = read_len(sk)
|
||||
if not n then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local p, err = sk:read(n + 2)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local s = ffi_str(p, n)
|
||||
local cmd = s:lower()
|
||||
cmd_line[1] = cmd
|
||||
|
||||
if cmd == "subscribe" or cmd == "psubscribe" then
|
||||
session.in_pub_sub = true
|
||||
end
|
||||
|
||||
local key_finder
|
||||
local matcher = session.matcher
|
||||
if matcher then
|
||||
matcher = matcher[s:lower()]
|
||||
if matcher then
|
||||
key_finder = commands.cmd_to_key_finder[s] or commands.default_key_finder
|
||||
end
|
||||
end
|
||||
|
||||
for i = 2, narg do
|
||||
local is_key = false
|
||||
if key_finder then
|
||||
is_key = key_finder(i, narg)
|
||||
end
|
||||
|
||||
local n, err = read_len(sk)
|
||||
if not n then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local s
|
||||
if not is_key and n > MAX_VALUE_LEN then
|
||||
-- avoid recording big value
|
||||
local p, err = sk:read(MAX_VALUE_LEN)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local ok, err = sk:drain(n - MAX_VALUE_LEN + 2)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
s = ffi_str(p, MAX_VALUE_LEN) .. "...(" .. n .. " bytes)"
|
||||
else
|
||||
local p, err = sk:read(n + 2)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
s = ffi_str(p, n)
|
||||
|
||||
if is_key and matcher.keys[s] then
|
||||
matcher = matcher.keys[s]
|
||||
key_finder = nil
|
||||
end
|
||||
end
|
||||
|
||||
cmd_line[i] = s
|
||||
end
|
||||
|
||||
session.req_id_seq = session.req_id_seq + 1
|
||||
local ctx = sdk.get_req_ctx(session, session.req_id_seq)
|
||||
ctx.cmd_line = cmd_line
|
||||
ctx.cmd = cmd
|
||||
|
||||
local pipelined = sk:has_pending_data()
|
||||
|
||||
if matcher then
|
||||
if matcher.keys then
|
||||
-- try to match any key of this command
|
||||
matcher = matcher.keys["*"]
|
||||
end
|
||||
|
||||
if matcher then
|
||||
sleep(matcher.delay)
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil, pipelined
|
||||
end
|
||||
|
||||
|
||||
local function read_subscribe_reply(sk)
|
||||
local line, err, n = read_line(sk)
|
||||
if not line then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local prefix = line[0]
|
||||
|
||||
if prefix == PREFIX_STR then -- char '$'
|
||||
local size = tonumber(ffi_str(line + 1, n - 1))
|
||||
if size < 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local p, err = sk:read(size + 2)
|
||||
if not p then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
return ffi_str(p, size)
|
||||
|
||||
elseif prefix == PREFIX_INT then -- char ':'
|
||||
return tonumber(ffi_str(line + 1, n - 1))
|
||||
|
||||
else
|
||||
return nil, str_fmt("unknown prefix: \"%s\"", prefix)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function read_reply(sk, session)
|
||||
local line, err, n = read_line(sk)
|
||||
if not line then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local prefix = line[0]
|
||||
|
||||
if prefix == PREFIX_STR then -- char '$'
|
||||
-- print("bulk reply")
|
||||
|
||||
local size = tonumber(ffi_str(line + 1, n - 1))
|
||||
if size < 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local ok, err = sk:drain(size + 2)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
elseif prefix == PREFIX_STA then -- char '+'
|
||||
-- print("status reply")
|
||||
return true
|
||||
|
||||
elseif prefix == PREFIX_ARR then -- char '*'
|
||||
local narr = tonumber(ffi_str(line + 1, n - 1))
|
||||
|
||||
-- print("multi-bulk reply: ", narr)
|
||||
if narr < 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
if session and session.in_pub_sub and (narr == 3 or narr == 4) then
|
||||
local msg_type, err = read_subscribe_reply(sk)
|
||||
if msg_type == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
session.pub_sub_msg_type = msg_type
|
||||
|
||||
local res, err = read_reply(sk)
|
||||
if res == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
if msg_type == "unsubscribe" or msg_type == "punsubscribe" then
|
||||
local n_ch, err = read_subscribe_reply(sk)
|
||||
if n_ch == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
if n_ch == 0 then
|
||||
session.in_pub_sub = -1
|
||||
-- clear this flag later at the end of `handle_reply`
|
||||
end
|
||||
|
||||
else
|
||||
local n = msg_type == "pmessage" and 2 or 1
|
||||
for i = 1, n do
|
||||
local res, err = read_reply(sk)
|
||||
if res == nil then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
for i = 1, narr do
|
||||
local res, err = read_reply(sk)
|
||||
if res == nil then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
elseif prefix == PREFIX_INT then -- char ':'
|
||||
-- print("integer reply")
|
||||
return true
|
||||
|
||||
elseif prefix == PREFIX_ERR then -- char '-'
|
||||
-- print("error reply: ", n)
|
||||
return true
|
||||
|
||||
else
|
||||
return nil, str_fmt("unknown prefix: \"%s\"", prefix)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function handle_reply(session, sk)
|
||||
local ok, err = read_reply(sk, session)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local ctx
|
||||
if session.in_pub_sub and session.pub_sub_msg_type then
|
||||
local msg_type = session.pub_sub_msg_type
|
||||
session.pub_sub_msg_type = nil
|
||||
if session.resp_id_seq < session.req_id_seq then
|
||||
local cur_ctx = sdk.get_req_ctx(session, session.resp_id_seq + 1)
|
||||
local cmd = cur_ctx.cmd
|
||||
if cmd == msg_type then
|
||||
ctx = cur_ctx
|
||||
session.resp_id_seq = session.resp_id_seq + 1
|
||||
end
|
||||
end
|
||||
|
||||
if session.in_pub_sub == -1 then
|
||||
session.in_pub_sub = nil
|
||||
end
|
||||
else
|
||||
session.resp_id_seq = session.resp_id_seq + 1
|
||||
ctx = sdk.get_req_ctx(session, session.resp_id_seq)
|
||||
end
|
||||
|
||||
return ctx
|
||||
end
|
||||
|
||||
|
||||
function _M.from_downstream(session, downstream)
|
||||
local read_pipeline = false
|
||||
while true do
|
||||
local ok, err, pipelined = read_req(session, downstream)
|
||||
if not ok then
|
||||
if err ~= "timeout" and err ~= "closed" then
|
||||
core.log.error("failed to read request: ", err)
|
||||
end
|
||||
|
||||
if read_pipeline and err == "timeout" then
|
||||
break
|
||||
end
|
||||
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
if not pipelined then
|
||||
break
|
||||
end
|
||||
|
||||
if not read_pipeline then
|
||||
read_pipeline = true
|
||||
-- set minimal read timeout to read pipelined data
|
||||
downstream:settimeouts(0, 0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
if read_pipeline then
|
||||
-- set timeout back
|
||||
downstream:settimeouts(0, 0, 0)
|
||||
end
|
||||
|
||||
return OK
|
||||
end
|
||||
|
||||
|
||||
function _M.connect_upstream(session, ctx)
|
||||
local conf = session.upstream_conf
|
||||
local nodes = conf.nodes
|
||||
if #nodes == 0 then
|
||||
core.log.error("failed to connect: no nodes")
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
local node = nodes[math_random(#nodes)]
|
||||
local sk = sdk.connect_upstream(node, conf)
|
||||
if not sk then
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
return OK, sk
|
||||
end
|
||||
|
||||
|
||||
function _M.disconnect_upstream(session, upstream)
|
||||
sdk.disconnect_upstream(upstream, session.upstream_conf)
|
||||
end
|
||||
|
||||
|
||||
function _M.to_upstream(session, ctx, downstream, upstream)
|
||||
local ok, err = upstream:move(downstream)
|
||||
if not ok then
|
||||
core.log.error("failed to send to upstream: ", err)
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
return OK
|
||||
end
|
||||
|
||||
|
||||
function _M.from_upstream(session, downstream, upstream)
|
||||
local ctx, err = handle_reply(session, upstream)
|
||||
if err then
|
||||
core.log.error("failed to handle upstream: ", err)
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
local ok, err = downstream:move(upstream)
|
||||
if not ok then
|
||||
core.log.error("failed to handle upstream: ", err)
|
||||
return DECLINED
|
||||
end
|
||||
|
||||
return DONE, ctx
|
||||
end
|
||||
|
||||
|
||||
function _M.log(session, ctx)
|
||||
local metrics = sdk.get_metrics(session, protocol_name)
|
||||
if metrics then
|
||||
session.cmd_labels[2] = ctx.cmd
|
||||
metrics.commands_total:inc(1, session.cmd_labels)
|
||||
metrics.commands_latency_seconds:observe(ctx.var.rpc_time, session.cmd_labels)
|
||||
end
|
||||
|
||||
core.tablepool.release("xrpc_redis_cmd_line", ctx.cmd_line)
|
||||
ctx.cmd_line = nil
|
||||
end
|
||||
|
||||
|
||||
return _M
|
@@ -0,0 +1,33 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local _M = {
|
||||
commands_total = {
|
||||
type = "counter",
|
||||
help = "Total number of requests for a specific Redis command",
|
||||
labels = {"route", "command"},
|
||||
},
|
||||
commands_latency_seconds = {
|
||||
type = "histogram",
|
||||
help = "Latency of requests for a specific Redis command",
|
||||
labels = {"route", "command"},
|
||||
-- latency buckets, 1ms to 1s:
|
||||
buckets = {0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
return _M
|
@@ -0,0 +1,59 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
-- contributor license agreements. See the NOTICE file distributed with
|
||||
-- this work for additional information regarding copyright ownership.
|
||||
-- The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
-- (the "License"); you may not use this file except in compliance with
|
||||
-- the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
--
|
||||
local core = require("apisix.core")
|
||||
|
||||
|
||||
local schema = {
|
||||
type = "object",
|
||||
properties = {
|
||||
faults = {
|
||||
type = "array",
|
||||
minItems = 1,
|
||||
items = {
|
||||
type = "object",
|
||||
properties = {
|
||||
commands = {
|
||||
type = "array",
|
||||
minItems = 1,
|
||||
items = {
|
||||
type = "string"
|
||||
},
|
||||
},
|
||||
key = {
|
||||
type = "string",
|
||||
minLength = 1,
|
||||
},
|
||||
delay = {
|
||||
type = "number",
|
||||
description = "additional delay in seconds",
|
||||
}
|
||||
},
|
||||
required = {"commands", "delay"}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local _M = {}
|
||||
|
||||
|
||||
function _M.check_schema(conf)
|
||||
return core.schema.check(schema, conf)
|
||||
end
|
||||
|
||||
|
||||
return _M
|
Reference in New Issue
Block a user