blob: 723108f221f29f6f850d71de6d13af9b34b31e39 [file]
#
# 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.
#
use t::APISIX 'no_plan';
repeat_each(2);
log_level('info');
no_root_location();
no_shuffle();
run_tests();
__DATA__
=== TEST 1: keep in-flight conn count across balancer recreation on scaling
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
-- resource_key is stable across scaling, so both pickers share the
-- same connection count table
local up = {resource_key = "/upstreams/lc-scale"}
-- 2 nodes serving long-lived connections
local nodes = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}
local p1 = least_conn.new(nodes, up, 0)
-- establish 4 in-flight connections (get without after_balance)
local ctx = {}
local held = {}
for _ = 1, 4 do
held[#held + 1] = p1.get(ctx)
end
-- scale out: add a third node, the picker is recreated
local scaled = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1,
["127.0.0.1:1982"] = 1}
local p2 = least_conn.new(scaled, up, 0)
-- the freshly added node has no connection, so it must be picked first
for _ = 1, 2 do
local s = p2.get(ctx)
held[#held + 1] = s
ngx.say(s)
end
-- release everything so repeated runs start from a clean state
for _, s in ipairs(held) do
ctx.balancer_server = s
p2.after_balance(ctx, false)
end
}
}
--- request
GET /t
--- response_body
127.0.0.1:1982
127.0.0.1:1982
=== TEST 2: a drained node returns to the pool across balancer recreation
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
local up = {resource_key = "/upstreams/lc-drain"}
local nodes = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}
local p1 = least_conn.new(nodes, up, 0)
-- hold 4 in-flight connections, 2 land on each node
local ctx = {}
local held = {}
for _ = 1, 4 do
held[#held + 1] = p1.get(ctx)
end
-- drain every connection on 1980
for _, s in ipairs(held) do
if s == "127.0.0.1:1980" then
ctx.balancer_server = s
p1.after_balance(ctx, false)
end
end
-- picker recreated: 1980 is back to baseline and must be preferred
-- over 1981 which is still holding connections
local p2 = least_conn.new(nodes, up, 0)
local s = p2.get(ctx)
ngx.say(s)
-- release the rest so repeated runs start from a clean state
ctx.balancer_server = s
p2.after_balance(ctx, false)
for _, h in ipairs(held) do
if h == "127.0.0.1:1981" then
ctx.balancer_server = h
p2.after_balance(ctx, false)
end
end
}
}
--- request
GET /t
--- response_body
127.0.0.1:1980
=== TEST 3: scale down drops the removed node, remaining nodes balance
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
local up = {resource_key = "/upstreams/lc-scale-down"}
local nodes = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}
local p1 = least_conn.new(nodes, up, 0)
local ctx = {}
-- fully complete two requests, one per node
for _ = 1, 2 do
local s = p1.get(ctx)
ctx.balancer_server = s
p1.after_balance(ctx, false)
end
-- scale down to a single remaining node, picker recreated
local scaled = {["127.0.0.1:1981"] = 1}
local p2 = least_conn.new(scaled, up, 0)
local s = p2.get(ctx)
ctx.balancer_server = s
p2.after_balance(ctx, false)
ngx.say(s)
}
}
--- request
GET /t
--- response_body
127.0.0.1:1981
=== TEST 4: connections released on the old picker are seen by the new one
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
local up = {resource_key = "/upstreams/lc-drain-old-picker"}
local nodes = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}
local p1 = least_conn.new(nodes, up, 0)
-- 4 long-lived connections, 2 on each node
local ctx = {}
local held = {}
for _ = 1, 4 do
held[#held + 1] = p1.get(ctx)
end
-- scale out: new requests are routed with a freshly built picker
local scaled = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1,
["127.0.0.1:1982"] = 1}
local p2 = least_conn.new(scaled, up, 0)
-- the long-lived connections close. They are still bound to the picker
-- they were routed with (ctx.server_picker), so they are released on p1
for _, s in ipairs(held) do
ctx.balancer_server = s
p1.after_balance(ctx, false)
end
-- every node is empty again, so the next requests must spread over all
-- of them instead of piling up on the node that was added last
local picked = {}
for _ = 1, 3 do
picked[#picked + 1] = p2.get(ctx)
end
table.sort(picked)
ngx.say(table.concat(picked, " "))
for _, s in ipairs(picked) do
ctx.balancer_server = s
p2.after_balance(ctx, false)
end
}
}
--- request
GET /t
--- response_body
127.0.0.1:1980 127.0.0.1:1981 127.0.0.1:1982
=== TEST 5: node sets of different priorities keep their own state
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
local up = {resource_key = "/upstreams/lc-priority"}
local high = least_conn.new({["127.0.0.1:1980"] = 1}, up, 1)
local low = least_conn.new({["127.0.0.1:1981"] = 1}, up, 0)
local ctx = {}
for _, picker in ipairs({high, low}) do
local s = picker.get(ctx)
ngx.say(s)
ctx.balancer_server = s
picker.after_balance(ctx, false)
end
}
}
--- request
GET /t
--- response_body
127.0.0.1:1980
127.0.0.1:1981
=== TEST 6: a request that runs out of servers does not release twice
--- config
location /t {
content_by_lua_block {
local balancer = require("apisix.balancer")
local least_conn = require("apisix.balancer.least_conn")
local up_conf = {
type = "least_conn",
resource_key = "/upstreams/lc-double-release",
nodes = {
{host = "127.0.0.1", port = 1980, weight = 10, priority = 0},
{host = "127.0.0.1", port = 1981, weight = 1, priority = 0},
},
}
local nodes = {["127.0.0.1:1980"] = 10, ["127.0.0.1:1981"] = 1}
-- three long-lived connections land on 1981 while it is the only node
local seeded = {}
local seed = least_conn.new({["127.0.0.1:1981"] = 1}, up_conf, 0)
for i = 1, 3 do
seeded[i] = {}
seeded[i].balancer_server = seed.get(seeded[i])
end
-- a request that fails on every node, driven through the real balancer
-- a fresh version, as a real scale out would produce, so the balancer
-- builds a picker for the two nodes over the state seeded above
local ctx = {upstream_conf = up_conf, upstream_version = tostring(ngx.now()),
upstream_key = "lc-double-release", var = {}}
assert(balancer.pick_server(nil, ctx), "first try")
assert(balancer.pick_server(nil, ctx), "retry")
local server, err = balancer.pick_server(nil, ctx)
ngx.say(server == nil and err or "expected to run out of servers")
-- the request holds nothing now, so the log phase must release nothing
ngx.say("holds a server: ", ctx.balancer_server ~= nil)
ctx.server_picker.after_balance(ctx, false)
-- 1981 still holds the three connections, so 1980 (ten times the weight)
-- must win every pick below. It only ties once 1981 is thought to be
-- lighter than it is
local picked = {}
local stolen = false
local p = least_conn.new(nodes, up_conf, 0)
for i = 1, 35 do
picked[i] = {}
picked[i].balancer_server = p.get(picked[i])
if picked[i].balancer_server == "127.0.0.1:1981" then
stolen = true
end
end
ngx.say(stolen and "stolen" or "kept")
for _, c in ipairs(picked) do
p.after_balance(c, false)
end
for _, c in ipairs(seeded) do
p.after_balance(c, false)
end
}
}
--- request
GET /t
--- response_body
failed to find valid upstream server, all upstream servers tried
holds a server: false
kept
=== TEST 7: scaling out an upstream with in-flight requests prefers the new node
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local http = require "resty.http"
-- the requests have to outlast the whole test, and the default upstream
-- read timeout (6s) would cut them short well before that
local function set_upstream(nodes)
local code, body = t('/apisix/admin/upstreams/1', ngx.HTTP_PUT,
[[{"type": "least_conn",
"timeout": {"connect": 60, "send": 60, "read": 60},
"nodes": ]] .. nodes .. [[}]])
assert(code < 300, body)
end
set_upstream([[{"127.0.0.1:1980": 1, "0.0.0.0:1980": 1}]])
local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT,
[[{"uri": "/mysleep", "upstream_id": "1"}]])
assert(code < 300, body)
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/mysleep?seconds="
-- let the route and the upstream reach the router before hitting them
ngx.sleep(1)
-- four in-flight requests, two on each of the original nodes
local threads = {}
for i = 1, 4 do
threads[i] = assert(ngx.thread.spawn(function ()
http.new():request_uri(uri .. "60")
end))
end
ngx.sleep(1)
-- scale out while they are still in flight
set_upstream([[{"127.0.0.1:1980": 1, "0.0.0.0:1980": 1, "127.0.0.2:1980": 1}]])
ngx.sleep(1)
-- the new node holds no connection, so it must take the next ones
for _ = 1, 2 do
assert(http.new():request_uri(uri .. "0.1"))
end
-- drop the clients. This only kills the client coroutines: the requests
-- they made stay parked on the upstream, so their counts are released
-- whenever those finish, not here. That is fine - a repeated run rebuilds
-- the upstream from one node, which drops the second one from the heap
-- and brings it back empty
for _, th in ipairs(threads) do
ngx.thread.kill(th)
end
}
}
--- request
GET /t
--- timeout: 10
--- grep_error_log eval
qr/proxy request to \S+/
--- grep_error_log_out eval
qr/\A(?:proxy request to (?:127\.0\.0\.1|0\.0\.0\.0):1980\n){4}(?:proxy request to 127\.0\.0\.2:1980\n){2}\z/
=== TEST 8: an upstream with no stable key keeps its state private to the picker
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
-- conf_server and ai-proxy-multi build pickers from an upstream table
-- that carries no resource key, so there is nothing to share state on
local nodes = {["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}
local p1 = least_conn.new(nodes, {})
local ctx1 = {}
ctx1.balancer_server = p1.get(ctx1)
-- a second picker starts empty: it must not see the connection held on
-- the first one, so it picks the very same node
local p2 = least_conn.new(nodes, {})
local ctx2 = {}
ctx2.balancer_server = p2.get(ctx2)
ngx.say(ctx2.balancer_server == ctx1.balancer_server and "private" or "shared")
p1.after_balance(ctx1, false)
p2.after_balance(ctx2, false)
}
}
--- request
GET /t
--- response_body
private
=== TEST 9: scaling out with live WebSocket sessions prefers the new node
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local ws_client = require("resty.websocket.client")
-- an upstream of its own: the balancing state is shared by every picker
-- built for one upstream, so a test that asserts on it cannot reuse the
-- upstream another test has been routing connections to
local function set_upstream(nodes)
local code, body = t('/apisix/admin/upstreams/2', ngx.HTTP_PUT,
[[{"type": "least_conn",
"timeout": {"connect": 60, "send": 60, "read": 60},
"nodes": ]] .. nodes .. [[}]])
assert(code < 300, body)
end
set_upstream([[{"127.0.0.1:1980": 1, "0.0.0.0:1980": 1}]])
local code, body = t('/apisix/admin/routes/2', ngx.HTTP_PUT,
[[{"uri": "/websocket_hold", "enable_websocket": true, "upstream_id": "2"}]])
assert(code < 300, body)
local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/websocket_hold"
ngx.sleep(1)
-- a session stays in flight until it is closed, which is what makes the
-- load stick to the nodes that were there before the scale out
local function hold()
local wb = ws_client:new({timeout = 60000})
assert(wb:connect(uri))
ngx.sleep(60)
end
local threads = {}
for i = 1, 4 do
threads[i] = assert(ngx.thread.spawn(hold))
end
ngx.sleep(1)
-- scale out while the four sessions are still connected
set_upstream([[{"127.0.0.1:1980": 1, "0.0.0.0:1980": 1, "127.0.0.2:1980": 1}]])
ngx.sleep(1)
-- the new node carries no session, so it must take the next ones
for i = 5, 6 do
threads[i] = assert(ngx.thread.spawn(hold))
end
ngx.sleep(1)
-- drop the clients. This only kills the client coroutines: the requests
-- they made stay parked on the upstream, so their counts are released
-- whenever those finish, not here. That is fine - a repeated run rebuilds
-- the upstream from one node, which drops the second one from the heap
-- and brings it back empty
for _, th in ipairs(threads) do
ngx.thread.kill(th)
end
}
}
--- request
GET /t
--- timeout: 10
--- grep_error_log eval
qr/proxy request to \S+/
--- grep_error_log_out eval
qr/\A(?:proxy request to (?:127\.0\.0\.1|0\.0\.0\.0):1980\n){4}(?:proxy request to 127\.0\.0\.2:1980\n){2}\z/
=== TEST 10: scaling a single-node upstream out counts the sessions it already has
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local http = require "resty.http"
-- the sessions have to outlast the whole test, and the default upstream
-- read timeout (6s) would cut them short well before that
local function set_upstream(nodes)
local code, body = t('/apisix/admin/upstreams/3', ngx.HTTP_PUT,
[[{"type": "least_conn",
"timeout": {"connect": 60, "send": 60, "read": 60},
"nodes": ]] .. nodes .. [[}]])
assert(code < 300, body)
end
-- a single node is the state a k8s deployment or a discovery service
-- starts from, and the requests routed while it was alone still have to
-- be counted, or the scale out cannot see that it is loaded
set_upstream([[{"127.0.0.1:1980": 1}]])
-- reuse route 1: two routes cannot both own /mysleep
local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT,
[[{"uri": "/mysleep", "upstream_id": "3"}]])
assert(code < 300, body)
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/mysleep?seconds="
ngx.sleep(1)
local threads = {}
for i = 1, 4 do
threads[i] = assert(ngx.thread.spawn(function ()
http.new():request_uri(uri .. "60")
end))
end
ngx.sleep(1)
set_upstream([[{"127.0.0.1:1980": 1, "127.0.0.2:1980": 1}]])
ngx.sleep(1)
-- the lone node holds four requests, the new one holds none
for _ = 1, 4 do
assert(http.new():request_uri(uri .. "0.1"))
end
-- abort the sessions rather than outwait them: the log phase still runs,
-- so the counts are released before the next run of this block
for _, th in ipairs(threads) do
ngx.thread.kill(th)
end
}
}
--- request
GET /t
--- timeout: 10
--- grep_error_log eval
qr/proxy request to \S+/
--- grep_error_log_out eval
qr/\A(?:proxy request to 127\.0\.0\.1:1980\n){4}(?:proxy request to 127\.0\.0\.2:1980\n){4}\z/
=== TEST 11: a weight change is applied without losing the connection count
--- config
location /t {
content_by_lua_block {
local least_conn = require("apisix.balancer.least_conn")
local up = {resource_key = "/upstreams/lc-reweight"}
local p1 = least_conn.new({["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 1}, up, 0)
-- one connection on each node: equal weight, equal score
local held = {}
for i = 1, 2 do
held[i] = {}
held[i].balancer_server = p1.get(held[i])
end
-- 1980 is given ten times the weight. It carries the same load as 1981,
-- so it is now the lighter of the two and must win the next picks
local p2 = least_conn.new({["127.0.0.1:1980"] = 10, ["127.0.0.1:1981"] = 1}, up, 0)
local picked = {}
for i = 1, 3 do
picked[i] = {}
picked[i].balancer_server = p2.get(picked[i])
ngx.say(picked[i].balancer_server)
end
for _, c in ipairs(picked) do
p2.after_balance(c, false)
end
for _, c in ipairs(held) do
p2.after_balance(c, false)
end
}
}
--- request
GET /t
--- response_body
127.0.0.1:1980
127.0.0.1:1980
127.0.0.1:1980