Enable session affinity for canaries (#7371)

This commit is contained in:
wasker 2021-07-29 14:23:19 -07:00 committed by GitHub
parent a327a809d9
commit f222c752be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1021 additions and 322 deletions

View file

@ -48,7 +48,7 @@ local function get_implementation(backend)
if backend["sessionAffinityConfig"] and
backend["sessionAffinityConfig"]["name"] == "cookie" then
if backend["sessionAffinityConfig"]["mode"] == 'persistent' then
if backend["sessionAffinityConfig"]["mode"] == "persistent" then
name = "sticky_persistent"
else
name = "sticky_balanced"
@ -186,6 +186,11 @@ local function sync_backends()
end
local function route_to_alternative_balancer(balancer)
if balancer.is_affinitized(balancer) then
-- If request is already affinitized to a primary balancer, keep the primary balancer.
return false
end
if not balancer.alternative_backends then
return false
end
@ -204,6 +209,13 @@ local function route_to_alternative_balancer(balancer)
return false
end
if alternative_balancer.is_affinitized(alternative_balancer) then
-- If request is affinitized to an alternative balancer, instruct caller to
-- switch to alternative.
return true
end
-- Use traffic shaping policy, if request didn't have affinity set.
local traffic_shaping_policy = alternative_balancer.traffic_shaping_policy
if not traffic_shaping_policy then
ngx.log(ngx.ERR, "traffic shaping policy is not set for balancer ",
@ -254,6 +266,10 @@ local function route_to_alternative_balancer(balancer)
return false
end
local function get_balancer_by_upstream_name(upstream_name)
return balancers[upstream_name]
end
local function get_balancer()
if ngx.ctx.balancer then
return ngx.ctx.balancer
@ -263,7 +279,7 @@ local function get_balancer()
local balancer = balancers[backend_name]
if not balancer then
return
return nil
end
if route_to_alternative_balancer(balancer) then
@ -352,6 +368,7 @@ setmetatable(_M, {__index = {
sync_backend = sync_backend,
route_to_alternative_balancer = route_to_alternative_balancer,
get_balancer = get_balancer,
get_balancer_by_upstream_name = get_balancer_by_upstream_name,
}})
return _M

View file

@ -68,6 +68,10 @@ function _M.new(self, backend)
return o
end
function _M.is_affinitized()
return false
end
function _M.balance(self)
local key = util.generate_var_value(self.hash_by)
local subset_id = self.instance:find(key)

View file

@ -170,6 +170,10 @@ local function calculate_slow_start_ewma(self)
return total_ewma / endpoints_count
end
function _M.is_affinitized()
return false
end
function _M.balance(self)
local peers = self.peers
local endpoint, ewma_score = peers[1], -1

View file

@ -14,6 +14,10 @@ function _M.new(self, o)
return o
end
function _M.is_affinitized()
return false
end
function _M.sync(self, backend)
self.traffic_shaping_policy = backend.trafficShapingPolicy
self.alternative_backends = backend.alternativeBackends

View file

@ -13,6 +13,7 @@ local setmetatable = setmetatable
local _M = balancer_resty:new()
local DEFAULT_COOKIE_NAME = "route"
local COOKIE_VALUE_DELIMITER = "|"
function _M.cookie_name(self)
return self.cookie_session_affinity.name or DEFAULT_COOKIE_NAME
@ -22,7 +23,8 @@ function _M.new(self)
local o = {
alternative_backends = nil,
cookie_session_affinity = nil,
traffic_shaping_policy = nil
traffic_shaping_policy = nil,
backend_key = nil
}
setmetatable(o, self)
@ -31,13 +33,37 @@ function _M.new(self)
return o
end
function _M.get_cookie(self)
function _M.get_cookie_parsed(self)
local cookie, err = ck:new()
if not cookie then
ngx.log(ngx.ERR, err)
end
return cookie:get(self:cookie_name())
local result = {
upstream_key = nil,
backend_key = nil
}
local raw_value = cookie:get(self:cookie_name())
if not raw_value then
return result
end
local parsed_value, len = split.split_string(raw_value, COOKIE_VALUE_DELIMITER)
if len == 0 then
return result
end
result.upstream_key = parsed_value[1]
if len > 1 then
result.backend_key = parsed_value[2]
end
return result
end
function _M.get_cookie(self)
return self:get_cookie_parsed().upstream_key
end
function _M.set_cookie(self, value)
@ -63,7 +89,7 @@ function _M.set_cookie(self, value)
local cookie_data = {
key = self:cookie_name(),
value = value,
value = value .. COOKIE_VALUE_DELIMITER .. self.backend_key,
path = cookie_path,
httponly = true,
samesite = cookie_samesite,
@ -86,6 +112,10 @@ function _M.set_cookie(self, value)
end
end
function _M.is_affinitized(self)
return self:get_cookie_parsed().backend_key == self.backend_key
end
function _M.get_last_failure()
return ngx_balancer.get_last_failure()
end
@ -166,6 +196,7 @@ function _M.sync(self, backend)
self.traffic_shaping_policy = backend.trafficShapingPolicy
self.alternative_backends = backend.alternativeBackends
self.cookie_session_affinity = backend.sessionAffinityConfig.cookieSessionAffinity
self.backend_key = ngx.md5(ngx.md5(backend.name) .. backend.name)
end
return _M

View file

@ -5,24 +5,35 @@ local util = require("util")
local original_ngx = ngx
function mock_ngx(mock)
local _ngx = mock
setmetatable(_ngx, {__index = _G.ngx})
_G.ngx = _ngx
end
local function reset_ngx()
_G.ngx = original_ngx
end
local function reset_sticky_balancer()
package.loaded["balancer.sticky"] = nil
package.loaded["balancer.sticky_balanced"] = nil
package.loaded["balancer.sticky_persistent"] = nil
sticky_balanced = require("balancer.sticky_balanced")
sticky_persistent = require("balancer.sticky_persistent")
end
local function mock_ngx(mock, after_mock_set)
local _ngx = mock
setmetatable(_ngx, { __index = ngx })
_G.ngx = _ngx
if after_mock_set then
after_mock_set()
end
-- Balancer module caches ngx module, must be reset after mocks were configured.
reset_sticky_balancer()
end
local function reset_ngx()
_G.ngx = original_ngx
-- Ensure balancer cache is reset.
_G.ngx.ctx.balancer = nil
end
function get_mocked_cookie_new()
local o = { value = nil }
local mock = {
@ -55,7 +66,6 @@ end
describe("Sticky", function()
before_each(function()
mock_ngx({ var = { location_path = "/", host = "test.com" } })
reset_sticky_balancer()
end)
after_each(function()
@ -65,29 +75,44 @@ describe("Sticky", function()
local test_backend = get_test_backend()
local test_backend_endpoint= test_backend.endpoints[1].address .. ":" .. test_backend.endpoints[1].port
local legacy_cookie_value = test_backend_endpoint
local function create_current_cookie_value(backend_key)
return test_backend_endpoint .. "|" .. backend_key
end
describe("new(backend)", function()
context("when backend specifies cookie name", function()
local function test(sticky)
local sticky_balancer_instance = sticky:new(test_backend)
describe("when backend specifies cookie name", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
local test_backend_cookie_name = test_backend.sessionAffinityConfig.cookieSessionAffinity.name
assert.equal(sticky_balancer_instance:cookie_name(), test_backend_cookie_name)
end
it("returns an instance containing the corresponding cookie name", function() test(sticky_balanced) end)
it("returns an instance containing the corresponding cookie name", function() test(sticky_persistent) end)
it("returns an instance containing the corresponding cookie name", function() test_with(sticky_balanced) end)
it("returns an instance containing the corresponding cookie name", function() test_with(sticky_persistent) end)
end)
context("when backend does not specify cookie name", function()
local function test(sticky)
describe("when backend does not specify cookie name", function()
local function test_with(sticky_balancer_type)
local temp_backend = util.deepcopy(test_backend)
temp_backend.sessionAffinityConfig.cookieSessionAffinity.name = nil
local sticky_balancer_instance = sticky:new(temp_backend)
local sticky_balancer_instance = sticky_balancer_type:new(temp_backend)
local default_cookie_name = "route"
assert.equal(sticky_balancer_instance:cookie_name(), default_cookie_name)
end
it("returns an instance with 'route' as cookie name", function() test(sticky_balanced) end)
it("returns an instance with 'route' as cookie name", function() test(sticky_persistent) end)
it("returns an instance with 'route' as cookie name", function() test_with(sticky_balanced) end)
it("returns an instance with 'route' as cookie name", function() test_with(sticky_persistent) end)
end)
describe("backend_key", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
assert.is_truthy(sticky_balancer_instance.backend_key)
end
it("calculates at construction time", function() test_with(sticky_balanced) end)
it("calculates at construction time", function() test_with(sticky_persistent) end)
end)
end)
@ -95,28 +120,25 @@ describe("Sticky", function()
local mocked_cookie_new = cookie.new
before_each(function()
package.loaded["balancer.sticky_balanced"] = nil
package.loaded["balancer.sticky_persistent"] = nil
sticky_balanced = require("balancer.sticky_balanced")
sticky_persistent = require("balancer.sticky_persistent")
reset_sticky_balancer()
end)
after_each(function()
cookie.new = mocked_cookie_new
end)
context("when client doesn't have a cookie set and location is in cookie_locations", function()
describe("when client doesn't have a cookie set and location is in cookie_locations", function()
local function test_pick_endpoint(sticky)
local sticky_balancer_instance = sticky:new(test_backend)
local function test_pick_endpoint_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
local peer = sticky_balancer_instance:balance()
assert.equal(test_backend_endpoint, peer)
end
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_balanced) end)
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_persistent) end)
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_balanced) end)
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_persistent) end)
local function test_set_cookie(sticky)
local function test_set_cookie_with(sticky_balancer_type)
local s = {}
cookie.new = function(self)
local cookie_instance = {
@ -137,15 +159,15 @@ describe("Sticky", function()
local b = get_test_backend()
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
local sticky_balancer_instance = sticky:new(b)
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("sets a cookie on the client", function() test_set_cookie(sticky_balanced) end)
it("sets a cookie on the client", function() test_set_cookie(sticky_persistent) end)
it("sets a cookie on the client", function() test_set_cookie_with(sticky_balanced) end)
it("sets a cookie on the client", function() test_set_cookie_with(sticky_persistent) end)
local function test_set_ssl_cookie(sticky)
local function test_set_ssl_cookie_with(sticky_balancer_type)
ngx.var.https = "on"
local s = {}
cookie.new = function(self)
@ -167,21 +189,17 @@ describe("Sticky", function()
local b = get_test_backend()
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
local sticky_balancer_instance = sticky:new(b)
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("sets a secure cookie on the client when being in ssl mode", function()
test_set_ssl_cookie(sticky_balanced)
end)
it("sets a secure cookie on the client when being in ssl mode", function()
test_set_ssl_cookie(sticky_persistent)
end)
it("sets a secure cookie on the client when being in ssl mode", function() test_set_ssl_cookie_with(sticky_balanced) end)
it("sets a secure cookie on the client when being in ssl mode", function() test_set_ssl_cookie_with(sticky_persistent) end)
end)
context("when client doesn't have a cookie set and cookie_locations contains a matching wildcard location",
function()
describe("when client doesn't have a cookie set and cookie_locations contains a matching wildcard location", function()
before_each(function ()
ngx.var.host = "dev.test.com"
end)
@ -189,7 +207,7 @@ describe("Sticky", function()
ngx.var.host = "test.com"
end)
local function test(sticky)
local function test_with(sticky_balancer_type)
local s = {}
cookie.new = function(self)
local cookie_instance = {
@ -211,27 +229,27 @@ describe("Sticky", function()
local b = get_test_backend()
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
b.sessionAffinityConfig.cookieSessionAffinity.locations["*.test.com"] = {"/"}
local sticky_balancer_instance = sticky:new(b)
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("sets a cookie on the client", function() test(sticky_balanced) end)
it("sets a cookie on the client", function() test(sticky_persistent) end)
it("sets a cookie on the client", function() test_with(sticky_balanced) end)
it("sets a cookie on the client", function() test_with(sticky_persistent) end)
end)
context("when client doesn't have a cookie set and location not in cookie_locations", function()
describe("when client doesn't have a cookie set and location not in cookie_locations", function()
local function test_pick_endpoint(sticky)
local sticky_balancer_instance = sticky:new(test_backend)
local function test_pick_endpoint_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
local peer = sticky_balancer_instance:balance()
assert.equal(peer, test_backend_endpoint)
end
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_balanced) end)
it("picks an endpoint for the client", function() test_pick_endpoint(sticky_persistent) end)
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_balanced) end)
it("picks an endpoint for the client", function() test_pick_endpoint_with(sticky_persistent) end)
local function test_no_cookie(sticky)
local function test_no_cookie_with(sticky_balancer_type)
local s = {}
cookie.new = function(self)
local cookie_instance = {
@ -248,34 +266,34 @@ describe("Sticky", function()
s = spy.on(cookie_instance, "set")
return cookie_instance, false
end
local sticky_balancer_instance = sticky:new(get_test_backend())
local sticky_balancer_instance = sticky_balancer_type:new(get_test_backend())
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_not_called()
end
it("does not set a cookie on the client", function() test_no_cookie(sticky_balanced) end)
it("does not set a cookie on the client", function() test_no_cookie(sticky_persistent) end)
it("does not set a cookie on the client", function() test_no_cookie_with(sticky_balanced) end)
it("does not set a cookie on the client", function() test_no_cookie_with(sticky_persistent) end)
end)
context("when client has a cookie set", function()
describe("when client has a cookie set", function()
local function test_no_cookie(sticky)
local function test_no_cookie_with(sticky_balancer_type)
local s = {}
cookie.new = function(self)
local return_obj = {
set = function(v) return false, nil end,
get = function(k) return test_backend_endpoint end,
get = function(k) return legacy_cookie_value end,
}
s = spy.on(return_obj, "set")
return return_obj, false
end
local sticky_balancer_instance = sticky:new(test_backend)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_not_called()
end
it("does not set a cookie", function() test_no_cookie(sticky_balanced) end)
it("does not set a cookie", function() test_no_cookie(sticky_persistent) end)
it("does not set a cookie", function() test_no_cookie_with(sticky_balanced) end)
it("does not set a cookie", function() test_no_cookie_with(sticky_persistent) end)
local function test_correct_endpoint(sticky)
local sticky_balancer_instance = sticky:new(test_backend)
@ -312,17 +330,16 @@ describe("Sticky", function()
before_each(function()
mock_ngx({ var = { location_path = "/", host = "test.com" } })
reset_sticky_balancer()
end)
after_each(function()
reset_ngx()
end)
context("when request to upstream fails", function()
describe("when request to upstream fails", function()
local function test(sticky, change_on_failure)
local sticky_balancer_instance = sticky:new(get_several_test_backends(change_on_failure))
local function test_with(sticky_balancer_type, change_on_failure)
local sticky_balancer_instance = sticky_balancer_type:new(get_several_test_backends(change_on_failure))
local old_upstream = sticky_balancer_instance:balance()
assert.is.Not.Nil(old_upstream)
@ -349,29 +366,21 @@ describe("Sticky", function()
end
end
it("changes upstream when change_on_failure option is true", function()
test(sticky_balanced, true)
end)
it("changes upstream when change_on_failure option is true", function()
test(sticky_balanced, false)
end)
it("changes upstream when change_on_failure option is true", function()
test(sticky_persistent, true)
end)
it("changes upstream when change_on_failure option is true", function()
test(sticky_persistent, false)
end)
it("changes upstream when change_on_failure option is true", function() test_with(sticky_balanced, true) end)
it("changes upstream when change_on_failure option is true", function() test_with(sticky_persistent, true) end)
it("changes upstream when change_on_failure option is false", function() test_with(sticky_balanced, false) end)
it("changes upstream when change_on_failure option is false", function() test_with(sticky_persistent, false) end)
end)
end)
context("when client doesn't have a cookie set and no host header, matching default server '_'",
function()
describe("when client doesn't have a cookie set and no host header, matching default server '_'", function()
before_each(function ()
ngx.var.host = "not-default-server"
ngx.var.server_name = "_"
end)
local function test(sticky)
local function test_with(sticky_balancer_type)
local s = {}
cookie.new = function(self)
local cookie_instance = {
@ -393,30 +402,27 @@ describe("Sticky", function()
local b = get_test_backend()
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
b.sessionAffinityConfig.cookieSessionAffinity.locations["_"] = {"/"}
local sticky_balancer_instance = sticky:new(b)
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("sets a cookie on the client", function() test(sticky_balanced) end)
it("sets a cookie on the client", function() test(sticky_persistent) end)
it("sets a cookie on the client", function() test_with(sticky_balanced) end)
it("sets a cookie on the client", function() test_with(sticky_persistent) end)
end)
describe("SameSite settings", function()
local mocked_cookie_new = cookie.new
before_each(function()
package.loaded["balancer.sticky_balanced"] = nil
package.loaded["balancer.sticky_persistent"] = nil
sticky_balanced = require("balancer.sticky_balanced")
sticky_persistent = require("balancer.sticky_persistent")
reset_sticky_balancer()
end)
after_each(function()
cookie.new = mocked_cookie_new
end)
local function test_set_cookie(sticky, samesite, conditional_samesite_none, expected_path, expected_samesite)
local function test_set_cookie_with(sticky_balancer_type, samesite, conditional_samesite_none, expected_path, expected_samesite)
local s = {}
cookie.new = function(self)
local cookie_instance = {
@ -439,34 +445,179 @@ describe("Sticky", function()
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
b.sessionAffinityConfig.cookieSessionAffinity.samesite = samesite
b.sessionAffinityConfig.cookieSessionAffinity.conditional_samesite_none = conditional_samesite_none
local sticky_balancer_instance = sticky:new(b)
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("returns a cookie with SameSite=Strict when user specifies samesite strict", function()
test_set_cookie(sticky_balanced, "Strict", false, "/", "Strict")
test_set_cookie_with(sticky_balanced, "Strict", false, "/", "Strict")
end)
it("returns a cookie with SameSite=Strict when user specifies samesite strict and conditional samesite none", function()
test_set_cookie(sticky_balanced, "Strict", true, "/", "Strict")
test_set_cookie_with(sticky_balanced, "Strict", true, "/", "Strict")
end)
it("returns a cookie with SameSite=Lax when user specifies samesite lax", function()
test_set_cookie(sticky_balanced, "Lax", false, "/", "Lax")
test_set_cookie_with(sticky_balanced, "Lax", false, "/", "Lax")
end)
it("returns a cookie with SameSite=Lax when user specifies samesite lax and conditional samesite none", function()
test_set_cookie(sticky_balanced, "Lax", true, "/", "Lax")
test_set_cookie_with(sticky_balanced, "Lax", true, "/", "Lax")
end)
it("returns a cookie with SameSite=None when user specifies samesite None", function()
test_set_cookie(sticky_balanced, "None", false, "/", "None")
test_set_cookie_with(sticky_balanced, "None", false, "/", "None")
end)
it("returns a cookie with SameSite=None when user specifies samesite None and conditional samesite none with supported user agent", function()
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.2704.103 Safari/537.36"} })
test_set_cookie(sticky_balanced, "None", true, "/", "None")
test_set_cookie_with(sticky_balanced, "None", true, "/", "None")
end)
it("returns a cookie without SameSite=None when user specifies samesite None and conditional samesite none with unsupported user agent", function()
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"} })
reset_sticky_balancer()
test_set_cookie(sticky_balanced, "None", true, "/", nil)
test_set_cookie_with(sticky_balanced, "None", true, "/", nil)
end)
it("returns a cookie with SameSite=Strict when user specifies samesite strict", function()
test_set_cookie_with(sticky_persistent, "Strict", false, "/", "Strict")
end)
it("returns a cookie with SameSite=Strict when user specifies samesite strict and conditional samesite none", function()
test_set_cookie_with(sticky_persistent, "Strict", true, "/", "Strict")
end)
it("returns a cookie with SameSite=Lax when user specifies samesite lax", function()
test_set_cookie_with(sticky_persistent, "Lax", false, "/", "Lax")
end)
it("returns a cookie with SameSite=Lax when user specifies samesite lax and conditional samesite none", function()
test_set_cookie_with(sticky_persistent, "Lax", true, "/", "Lax")
end)
it("returns a cookie with SameSite=None when user specifies samesite None", function()
test_set_cookie_with(sticky_persistent, "None", false, "/", "None")
end)
it("returns a cookie with SameSite=None when user specifies samesite None and conditional samesite none with supported user agent", function()
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.2704.103 Safari/537.36"} })
test_set_cookie_with(sticky_persistent, "None", true, "/", "None")
end)
it("returns a cookie without SameSite=None when user specifies samesite None and conditional samesite none with unsupported user agent", function()
mock_ngx({ var = { location_path = "/", host = "test.com" , http_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"} })
test_set_cookie_with(sticky_persistent, "None", true, "/", nil)
end)
end)
describe("get_cookie()", function()
describe("legacy cookie value", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
cookie.new = function(self)
local return_obj = {
set = function(v) return false, nil end,
get = function(k) return legacy_cookie_value end,
}
return return_obj, false
end
assert.equal(test_backend_endpoint, sticky_balancer_instance.get_cookie(sticky_balancer_instance))
end
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
end)
describe("current cookie value", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
cookie.new = function(self)
local return_obj = {
set = function(v) return false, nil end,
get = function(k) return create_current_cookie_value(sticky_balancer_instance.backend_key) end,
}
return return_obj, false
end
assert.equal(test_backend_endpoint, sticky_balancer_instance.get_cookie(sticky_balancer_instance))
end
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
end)
end)
describe("get_cookie_parsed()", function()
describe("legacy cookie value", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
cookie.new = function(self)
local return_obj = {
set = function(v) return false, nil end,
get = function(k) return legacy_cookie_value end,
}
return return_obj, false
end
local parsed_cookie = sticky_balancer_instance.get_cookie_parsed(sticky_balancer_instance)
assert.is_truthy(parsed_cookie)
assert.equal(test_backend_endpoint, parsed_cookie.upstream_key)
assert.is_falsy(parsed_cookie.backend_key)
end
it("retrieves upstream key value", function() test_with(sticky_balanced) end)
it("retrieves upstream key value", function() test_with(sticky_persistent) end)
end)
describe("current cookie value", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
cookie.new = function(self)
local return_obj = {
set = function(v) return false, nil end,
get = function(k) return create_current_cookie_value(sticky_balancer_instance.backend_key) end,
}
return return_obj, false
end
local parsed_cookie = sticky_balancer_instance.get_cookie_parsed(sticky_balancer_instance)
assert.is_truthy(parsed_cookie)
assert.equal(test_backend_endpoint, parsed_cookie.upstream_key)
assert.equal(sticky_balancer_instance.backend_key, parsed_cookie.backend_key)
end
it("retrieves all supported values", function() test_with(sticky_balanced) end)
it("retrieves all supported values", function() test_with(sticky_persistent) end)
end)
end)
describe("set_cookie()", function()
local function test_with(sticky_balancer_type)
local sticky_balancer_instance = sticky_balancer_type:new(test_backend)
local cookieSetSpy = {}
cookie.new = function(self)
local return_obj = {
set = function(self, payload)
assert.equal(create_current_cookie_value(sticky_balancer_instance.backend_key), payload.value)
return true, nil
end,
get = function(k) return nil end,
}
cookieSetSpy = spy.on(return_obj, "set")
return return_obj, false
end
sticky_balancer_instance.set_cookie(sticky_balancer_instance, test_backend_endpoint)
assert.spy(cookieSetSpy).was_called()
end
it("constructs correct cookie value", function() test_with(sticky_balanced) end)
it("constructs correct cookie value", function() test_with(sticky_persistent) end)
end)
end)

View file

@ -6,12 +6,9 @@ local original_ngx = ngx
local function reset_ngx()
_G.ngx = original_ngx
end
local function mock_ngx(mock)
local _ngx = mock
setmetatable(_ngx, { __index = ngx })
_G.ngx = _ngx
-- Ensure balancer cache is reset.
_G.ngx.ctx.balancer = nil
end
local function reset_balancer()
@ -19,6 +16,19 @@ local function reset_balancer()
balancer = require("balancer")
end
local function mock_ngx(mock, after_mock_set)
local _ngx = mock
setmetatable(_ngx, { __index = ngx })
_G.ngx = _ngx
if after_mock_set then
after_mock_set()
end
-- Balancer module caches ngx module, must be reset after mocks were configured.
reset_balancer()
end
local function reset_expected_implementations()
expected_implementations = {
["access-router-production-web-80"] = package.loaded["balancer.round_robin"],
@ -26,7 +36,8 @@ local function reset_expected_implementations()
["my-dummy-app-2"] = package.loaded["balancer.chash"],
["my-dummy-app-3"] = package.loaded["balancer.sticky_persistent"],
["my-dummy-app-4"] = package.loaded["balancer.ewma"],
["my-dummy-app-5"] = package.loaded["balancer.sticky_balanced"]
["my-dummy-app-5"] = package.loaded["balancer.sticky_balanced"],
["my-dummy-app-6"] = package.loaded["balancer.chashsubset"]
}
end
@ -48,20 +59,35 @@ local function reset_backends()
cookie = ""
},
},
{ name = "my-dummy-app-1", ["load-balance"] = "round_robin", },
{
name = "my-dummy-app-2", ["load-balance"] = "chash",
name = "my-dummy-app-1",
["load-balance"] = "round_robin",
},
{
name = "my-dummy-app-2",
["load-balance"] = "round_robin", -- upstreamHashByConfig will take priority.
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
},
{
name = "my-dummy-app-3", ["load-balance"] = "ewma",
sessionAffinityConfig = { name = "cookie", mode = 'persistent', cookieSessionAffinity = { name = "route" } }
name = "my-dummy-app-3",
["load-balance"] = "ewma", -- sessionAffinityConfig will take priority.
sessionAffinityConfig = { name = "cookie", mode = "persistent", cookieSessionAffinity = { name = "route" } }
},
{ name = "my-dummy-app-4", ["load-balance"] = "ewma", },
{
name = "my-dummy-app-5", ["load-balance"] = "ewma", ["upstream-hash-by"] = "$request_uri",
name = "my-dummy-app-4",
["load-balance"] = "ewma",
},
{
name = "my-dummy-app-5",
["load-balance"] = "ewma", -- sessionAffinityConfig will take priority.
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", },
sessionAffinityConfig = { name = "cookie", cookieSessionAffinity = { name = "route" } }
},
{
name = "my-dummy-app-6",
["load-balance"] = "ewma", -- upstreamHashByConfig will take priority.
upstreamHashByConfig = { ["upstream-hash-by"] = "$request_uri", ["upstream-hash-by-subset"] = "true", }
},
}
end
@ -77,7 +103,7 @@ describe("Balancer", function()
end)
describe("get_implementation()", function()
it("returns correct implementation for given backend", function()
it("uses heuristics to select correct load balancer implementation for a given backend", function()
for _, backend in pairs(backends) do
local expected_implementation = expected_implementations[backend.name]
local implementation = balancer.get_implementation(backend)
@ -89,8 +115,8 @@ describe("Balancer", function()
describe("get_balancer()", function()
it("always returns the same balancer for given request context", function()
local backend = {
name = "my-dummy-app-6", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-6" },
name = "my-dummy-app-100", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-100" },
endpoints = { { address = "10.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
trafficShapingPolicy = {
weight = 0,
@ -100,8 +126,8 @@ describe("Balancer", function()
},
}
local canary_backend = {
name = "my-dummy-canary-app-6", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-6" },
name = "my-dummy-canary-app-100", ["load-balance"] = "ewma",
alternativeBackends = { "my-dummy-canary-app-100" },
endpoints = { { address = "11.184.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
trafficShapingPolicy = {
weight = 5,
@ -112,7 +138,6 @@ describe("Balancer", function()
}
mock_ngx({ var = { proxy_upstream_name = backend.name } })
reset_balancer()
balancer.sync_backend(backend)
balancer.sync_backend(canary_backend)
@ -126,172 +151,223 @@ describe("Balancer", function()
end)
describe("route_to_alternative_balancer()", function()
local backend, _balancer
local backend, _primaryBalancer
before_each(function()
backend = backends[1]
_balancer = {
_primaryBalancer = {
alternative_backends = {
backend.name,
}
}
mock_ngx({ var = { request_uri = "/" } })
reset_balancer()
end)
it("returns false when no trafficShapingPolicy is set", function()
balancer.sync_backend(backend)
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
end)
-- Not affinitized request must follow traffic shaping policies.
describe("not affinitized", function()
it("returns false when no alternative backends is set", function()
backend.trafficShapingPolicy.weight = 100
balancer.sync_backend(backend)
_balancer.alternative_backends = nil
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
end)
before_each(function()
_primaryBalancer.is_affinitized = function (_)
return false
end
end)
it("returns false when alternative backends name does not match", function()
backend.trafficShapingPolicy.weight = 100
balancer.sync_backend(backend)
_balancer.alternative_backends[1] = "nonExistingBackend"
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
end)
it("returns false when no trafficShapingPolicy is set", function()
balancer.sync_backend(backend)
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
end)
context("canary by weight", function()
it("returns true when weight is 100", function()
it("returns false when no alternative backends is set", function()
backend.trafficShapingPolicy.weight = 100
balancer.sync_backend(backend)
assert.equal(true, balancer.route_to_alternative_balancer(_balancer))
_primaryBalancer.alternative_backends = nil
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
end)
it("returns false when weight is 0", function()
backend.trafficShapingPolicy.weight = 0
it("returns false when alternative backends name does not match", function()
backend.trafficShapingPolicy.weight = 100
balancer.sync_backend(backend)
assert.equal(false, balancer.route_to_alternative_balancer(_balancer))
_primaryBalancer.alternative_backends[1] = "nonExistingBackend"
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
end)
describe("canary by weight", function()
it("returns true when weight is 100", function()
backend.trafficShapingPolicy.weight = 100
balancer.sync_backend(backend)
assert.equal(true, balancer.route_to_alternative_balancer(_primaryBalancer))
end)
it("returns false when weight is 0", function()
backend.trafficShapingPolicy.weight = 0
balancer.sync_backend(backend)
assert.equal(false, balancer.route_to_alternative_balancer(_primaryBalancer))
end)
end)
describe("canary by cookie", function()
it("returns correct result for given cookies", function()
local test_patterns = {
{
case_title = "cookie_value is 'always'",
request_cookie_name = "canaryCookie",
request_cookie_value = "always",
expected_result = true,
},
{
case_title = "cookie_value is 'never'",
request_cookie_name = "canaryCookie",
request_cookie_value = "never",
expected_result = false,
},
{
case_title = "cookie_value is undefined",
request_cookie_name = "canaryCookie",
request_cookie_value = "foo",
expected_result = false,
},
{
case_title = "cookie_name is undefined",
request_cookie_name = "foo",
request_cookie_value = "always",
expected_result = false
},
}
for _, test_pattern in pairs(test_patterns) do
mock_ngx({ var = {
["cookie_" .. test_pattern.request_cookie_name] = test_pattern.request_cookie_value,
request_uri = "/"
}})
backend.trafficShapingPolicy.cookie = "canaryCookie"
balancer.sync_backend(backend)
assert.message("\nTest data pattern: " .. test_pattern.case_title)
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_primaryBalancer))
reset_ngx()
end
end)
end)
describe("canary by header", function()
it("returns correct result for given headers", function()
local test_patterns = {
-- with no header value setting
{
case_title = "no custom header value and header value is 'always'",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "always",
expected_result = true,
},
{
case_title = "no custom header value and header value is 'never'",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "never",
expected_result = false,
},
{
case_title = "no custom header value and header value is undefined",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "foo",
expected_result = false,
},
{
case_title = "no custom header value and header name is undefined",
header_name = "canaryHeader",
header_value = "",
request_header_name = "foo",
request_header_value = "always",
expected_result = false,
},
-- with header value setting
{
case_title = "custom header value is set and header value is 'always'",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "canaryHeader",
request_header_value = "always",
expected_result = false,
},
{
case_title = "custom header value is set and header value match custom header value",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "canaryHeader",
request_header_value = "foo",
expected_result = true,
},
{
case_title = "custom header value is set and header name is undefined",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "bar",
request_header_value = "foo",
expected_result = false
},
}
for _, test_pattern in pairs(test_patterns) do
mock_ngx({ var = {
["http_" .. test_pattern.request_header_name] = test_pattern.request_header_value,
request_uri = "/"
}})
backend.trafficShapingPolicy.header = test_pattern.header_name
backend.trafficShapingPolicy.headerValue = test_pattern.header_value
balancer.sync_backend(backend)
assert.message("\nTest data pattern: " .. test_pattern.case_title)
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_primaryBalancer))
reset_ngx()
end
end)
end)
end)
context("canary by cookie", function()
it("returns correct result for given cookies", function()
local test_patterns = {
{
case_title = "cookie_value is 'always'",
request_cookie_name = "canaryCookie",
request_cookie_value = "always",
expected_result = true,
},
{
case_title = "cookie_value is 'never'",
request_cookie_name = "canaryCookie",
request_cookie_value = "never",
expected_result = false,
},
{
case_title = "cookie_value is undefined",
request_cookie_name = "canaryCookie",
request_cookie_value = "foo",
expected_result = false,
},
{
case_title = "cookie_name is undefined",
request_cookie_name = "foo",
request_cookie_value = "always",
expected_result = false
},
}
for _, test_pattern in pairs(test_patterns) do
mock_ngx({ var = {
["cookie_" .. test_pattern.request_cookie_name] = test_pattern.request_cookie_value,
request_uri = "/"
}})
reset_balancer()
backend.trafficShapingPolicy.cookie = "canaryCookie"
balancer.sync_backend(backend)
assert.message("\nTest data pattern: " .. test_pattern.case_title)
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_balancer))
reset_ngx()
end
end)
end)
-- Affinitized request prefers backend it is affinitized to.
describe("affinitized", function()
context("canary by header", function()
it("returns correct result for given headers", function()
local test_patterns = {
-- with no header value setting
{
case_title = "no custom header value and header value is 'always'",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "always",
expected_result = true,
},
{
case_title = "no custom header value and header value is 'never'",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "never",
expected_result = false,
},
{
case_title = "no custom header value and header value is undefined",
header_name = "canaryHeader",
header_value = "",
request_header_name = "canaryHeader",
request_header_value = "foo",
expected_result = false,
},
{
case_title = "no custom header value and header name is undefined",
header_name = "canaryHeader",
header_value = "",
request_header_name = "foo",
request_header_value = "always",
expected_result = false,
},
-- with header value setting
{
case_title = "custom header value is set and header value is 'always'",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "canaryHeader",
request_header_value = "always",
expected_result = false,
},
{
case_title = "custom header value is set and header value match custom header value",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "canaryHeader",
request_header_value = "foo",
expected_result = true,
},
{
case_title = "custom header value is set and header name is undefined",
header_name = "canaryHeader",
header_value = "foo",
request_header_name = "bar",
request_header_value = "foo",
expected_result = false
},
}
for _, test_pattern in pairs(test_patterns) do
mock_ngx({ var = {
["http_" .. test_pattern.request_header_name] = test_pattern.request_header_value,
request_uri = "/"
}})
reset_balancer()
backend.trafficShapingPolicy.header = test_pattern.header_name
backend.trafficShapingPolicy.headerValue = test_pattern.header_value
balancer.sync_backend(backend)
assert.message("\nTest data pattern: " .. test_pattern.case_title)
.equal(test_pattern.expected_result, balancer.route_to_alternative_balancer(_balancer))
reset_ngx()
end
before_each(function()
mock_ngx({ var = { request_uri = "/", proxy_upstream_name = backend.name } })
balancer.sync_backend(backend)
end)
it("returns false if request is affinitized to primary backend", function()
_primaryBalancer.is_affinitized = function (_)
return true
end
local alternativeBalancer = balancer.get_balancer_by_upstream_name(backend.name)
local primarySpy = spy.on(_primaryBalancer, "is_affinitized")
local alternativeSpy = spy.on(alternativeBalancer, "is_affinitized")
assert.is_false(balancer.route_to_alternative_balancer(_primaryBalancer))
assert.spy(_primaryBalancer.is_affinitized).was_called()
assert.spy(alternativeBalancer.is_affinitized).was_not_called()
end)
it("returns true if request is affinitized to alternative backend", function()
_primaryBalancer.is_affinitized = function (_)
return false
end
local alternativeBalancer = balancer.get_balancer_by_upstream_name(backend.name)
alternativeBalancer.is_affinitized = function (_)
return true
end
local primarySpy = spy.on(_primaryBalancer, "is_affinitized")
local alternativeSpy = spy.on(alternativeBalancer, "is_affinitized")
assert.is_true(balancer.route_to_alternative_balancer(_primaryBalancer))
assert.spy(_primaryBalancer.is_affinitized).was_called()
assert.spy(alternativeBalancer.is_affinitized).was_called()
end)
end)
end)
@ -432,10 +508,13 @@ describe("Balancer", function()
},
}
}
mock_ngx({ var = { proxy_upstream_name = "access-router-production-web-80" }, ctx = { } })
ngx.shared.configuration_data:set("backends", cjson.encode(backends))
reset_balancer()
mock_ngx({ var = { proxy_upstream_name = "access-router-production-web-80" }, ctx = { } }, function()
ngx.shared.configuration_data:set("backends", cjson.encode(backends))
end)
balancer.init_worker()
assert.not_equal(balancer.get_balancer(), nil)
end)

View file

@ -1,15 +0,0 @@
local split = require("util.split")
describe("split", function()
it("get_last_value", function()
for _, case in ipairs({
{"127.0.0.1:26157 : 127.0.0.1:26158", "127.0.0.1:26158"},
{"127.0.0.1:26157, 127.0.0.1:26158", "127.0.0.1:26158"},
{"127.0.0.1:26158", "127.0.0.1:26158"},
}) do
local last = split.get_last_value(case[1])
assert.equal(case[2], last)
end
end)
end)

View file

@ -0,0 +1,57 @@
local split = require("util.split")
describe("split", function()
describe("get_last_value", function()
it("splits value of an upstream variable and returns last value", function()
for _, case in ipairs({{"127.0.0.1:26157 : 127.0.0.1:26158", "127.0.0.1:26158"},
{"127.0.0.1:26157, 127.0.0.1:26158", "127.0.0.1:26158"},
{"127.0.0.1:26158", "127.0.0.1:26158"}}) do
local last = split.get_last_value(case[1])
assert.equal(case[2], last)
end
end)
end)
describe("split_string", function()
it("returns empty array if input string is empty", function()
local splits, len = split.split_string("", ",")
assert.equal(0, len)
assert.is.truthy(splits)
end)
it("returns empty array if input string is nil", function()
local splits, len = split.split_string(nil, ",")
assert.equal(0, len)
assert.is.truthy(splits)
end)
it("returns empty array if delimiter is empty", function()
local splits, len = split.split_string("1,2", "")
assert.equal(0, len)
assert.is.truthy(splits)
end)
it("returns empty array delimiter is nil", function()
local splits, len = split.split_string("1,2", nil)
assert.equal(0, len)
assert.is.truthy(splits)
end)
it("returns array of 1 value if input string is not a list", function()
local splits, len = split.split_string("123", ",")
assert.equal(1, len)
assert.equal("123", splits[1])
end)
it("returns array of values extracted from the input string", function()
local splits, len = split.split_string("1,2,3", ",")
assert.equal(3, len)
assert.equal("1", splits[1])
assert.equal("2", splits[2])
assert.equal("3", splits[3])
end)
end)
end)

View file

@ -65,4 +65,19 @@ function _M.split_upstream_addr(addrs_str)
return host_and_ports
end
-- Splits string by delimiter. Returns array of parsed values and the length of the array.
function _M.split_string(what, delim)
local result = {}
local idx = 0
if what and delim and delim ~= "" then
for chunk in what:gmatch("([^" .. delim .. "]+)") do
idx = idx + 1
result[idx] = chunk
end
end
return result, idx
end
return _M