Skip to content

Commit 12020d8

Browse files
authored
Merge pull request #1585 from tkan145/refactor-routing-policy
perf(routing): eliminate unnecessary allocations per request
2 parents 63e3509 + 8e6e15b commit 12020d8

6 files changed

Lines changed: 139 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Performance
1111
- Camel Policy: avoid unnecessary closure allocation per request [PR #1584](https://github.com/3scale/APIcast/pull/1584)
1212
- Logging Policy: construct all templates at init time [PR #1587](https://github.com/3scale/APIcast/pull/1587)
13+
- Routing Policy: eliminate unnecessary TemplateString allocation per request [PR #1585](https://github.com/3scale/APIcast/pull/1585)
1314

1415
### Fixed
1516
- Correct FAPI header to `x-fapi-interaction-id` [PR #1557](https://github.com/3scale/APIcast/pull/1557) [THREESCALE-11957](https://issues.redhat.com/browse/THREESCALE-11957)

gateway/src/apicast/policy/routing/routing.lua

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ function _M.new(config)
3737
return self
3838
end
3939

40+
local function route_upstream_usage_cleanup(self, usage, matched_rules)
41+
if not self.route_upstream then
42+
return
43+
end
44+
45+
local usage_diff = mapping_rules_matcher.clean_usage_by_owner_id(
46+
matched_rules , self.route_upstream:has_owner_id())
47+
usage:merge(usage_diff)
48+
end
49+
4050
function _M:access(context)
4151
-- All route definition needs to happen in the access phase to make sure that
4252
-- the mapping rule with the owner_id happens before the APIcast policy and
@@ -57,16 +67,7 @@ function _M:access(context)
5767

5868
-- this function substract the usage that does not match with the owner_id by
5969
-- the matched_rules
60-
context.route_upstream_usage_cleanup = function(self, usage, matched_rules)
61-
if not self.route_upstream then
62-
return
63-
end
64-
65-
local usage_diff = mapping_rules_matcher.clean_usage_by_owner_id(
66-
matched_rules , self.route_upstream:has_owner_id())
67-
usage:merge(usage_diff)
68-
end
69-
70+
context.route_upstream_usage_cleanup = route_upstream_usage_cleanup
7071
end
7172

7273

gateway/src/apicast/policy/routing/routing_operation.lua

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,31 @@
66
-- to get the left operand instead of the operand itself.
77

88
local setmetatable = setmetatable
9-
local Operation = require('apicast.conditions.operation')
9+
local match = ngx.re.match
1010
local TemplateString = require('apicast.template_string')
1111

1212
local _M = {}
1313

1414
local mt = { __index = _M }
1515

16+
local evaluate_func = {
17+
['=='] = function(left, right) return tostring(left) == tostring(right) end,
18+
['!='] = function(left, right) return tostring(left) ~= tostring(right) end,
19+
20+
-- Implemented on top of ngx.re.match. Returns true when there is a match and
21+
-- false otherwise.
22+
['matches'] = function(left, right)
23+
return (match(tostring(left), tostring(right)) and true) or false
24+
end
25+
}
26+
1627
local function new(evaluate_left_side_func, op, value, value_type)
1728
local self = setmetatable({}, mt)
1829

1930
self.evaluate_left_side_func = evaluate_left_side_func
20-
self.op = op
21-
self.value = value
22-
self.value_type = value_type
31+
self.evaluate_func = evaluate_func[op]
32+
assert(self.evaluate_func, 'Unsupported operation: ' .. (op or 'nil'))
33+
self.right_template = TemplateString.new(value, value_type or 'plain')
2334

2435
return self
2536
end
@@ -55,21 +66,18 @@ function _M.new_op_with_jwt_claim(jwt_claim_name, op, value, value_type)
5566
end
5667

5768
function _M.new_op_with_liquid_templating(liquid_expression, op, value, value_type)
69+
local template = TemplateString.new(liquid_expression or "", "liquid")
5870
local eval_left_func = function(context)
59-
return TemplateString.new(liquid_expression or "" , "liquid"):render(context)
71+
return template:render(context)
6072
end
6173

6274
return new(eval_left_func, op, value, value_type)
6375
end
6476

6577
function _M:evaluate(context)
66-
local left_operand_val = self.evaluate_left_side_func(context)
67-
68-
local op = Operation.new(
69-
left_operand_val, 'plain', self.op, self.value, self.value_type
70-
)
71-
72-
return op:evaluate(context)
78+
local left = self.evaluate_left_side_func(context)
79+
local right = self.right_template:render(context)
80+
return self.evaluate_func(left, right)
7381
end
7482

7583
return _M

gateway/src/apicast/policy/routing/rule.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ function _M.new_from_config_rule(config_rule)
7474
self.owner_id = tonumber(config_rule.owner_id)
7575
return self
7676
else
77-
return nil, 'failed to initialize upstream from url: ',
78-
config_rule.url, ' err: ', err
77+
return nil, 'failed to initialize upstream from url: ' ..
78+
config_rule.url .. ' err: ' .. (err or 'unknown')
7979
end
8080
end
8181

spec/policy/routing/routing_operation_spec.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,46 @@ describe('RoutingOperation', function()
3131

3232
assert.is_false(operation:evaluate(context))
3333
end)
34+
35+
it('evaluates != correctly when paths differ', function()
36+
local operation = RoutingOperation.new_op_with_path('!=', '/expected')
37+
38+
local context = {
39+
request = { get_uri = function() return '/other' end }
40+
}
41+
42+
assert.is_true(operation:evaluate(context))
43+
end)
44+
45+
it('evaluates != correctly when paths match', function()
46+
local operation = RoutingOperation.new_op_with_path('!=', '/same')
47+
48+
local context = {
49+
request = { get_uri = function() return '/same' end }
50+
}
51+
52+
assert.is_false(operation:evaluate(context))
53+
end)
54+
55+
it('evaluates matches true when regex matches', function()
56+
local operation = RoutingOperation.new_op_with_path('matches', '^/api/.*')
57+
58+
local context = {
59+
request = { get_uri = function() return '/api/users' end }
60+
}
61+
62+
assert.is_true(operation:evaluate(context))
63+
end)
64+
65+
it('evaluates matches false when regex does not match', function()
66+
local operation = RoutingOperation.new_op_with_path('matches', '^/api/.*')
67+
68+
local context = {
69+
request = { get_uri = function() return '/other/path' end }
70+
}
71+
72+
assert.is_false(operation:evaluate(context))
73+
end)
3474
end)
3575

3676
describe('when the operation involves a header', function()
@@ -236,6 +276,12 @@ describe('RoutingOperation', function()
236276

237277
end)
238278

279+
it('raises an error for unsupported operators', function()
280+
assert.has_error(function()
281+
RoutingOperation.new_op_with_path('unsupported_op', '/path')
282+
end)
283+
end)
284+
239285
it('can evaluate the right operand as liquid', function()
240286
-- Stub the available context to avoid depending on ngx.var.*
241287
stub(ngx_variable, 'available_context', function(context) return context end)

spec/policy/routing/routing_spec.lua

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,67 @@ local RoutingPolicy = require('apicast.policy.routing')
22
local UpstreamSelector = require('apicast.policy.routing.upstream_selector')
33
local Request = require('apicast.policy.routing.request')
44
local Upstream = require('apicast.upstream')
5+
local mapping_rules_matcher = require('apicast.mapping_rules_matcher')
56

67
describe('Routing policy', function()
8+
describe('.access', function()
9+
it('assigns route_upstream_usage_cleanup to the context', function()
10+
local routing = RoutingPolicy.new()
11+
local context = {}
12+
routing:access(context)
13+
14+
assert.is_function(context.route_upstream_usage_cleanup)
15+
end)
16+
17+
it('assigns the same function reference on every call', function()
18+
local routing = RoutingPolicy.new()
19+
local ctx1 = {}
20+
local ctx2 = {}
21+
routing:access(ctx1)
22+
routing:access(ctx2)
23+
24+
assert.equals(ctx1.route_upstream_usage_cleanup, ctx2.route_upstream_usage_cleanup)
25+
end)
26+
end)
27+
28+
describe('route_upstream_usage_cleanup', function()
29+
it('is a no-op when route_upstream is nil', function()
30+
local routing = RoutingPolicy.new()
31+
local context = {}
32+
routing:access(context)
33+
34+
assert.has_no_errors(function()
35+
context:route_upstream_usage_cleanup({}, {})
36+
end)
37+
end)
38+
39+
it('calls clean_usage_by_owner_id and merges usage when route_upstream exists', function()
40+
local routing = RoutingPolicy.new()
41+
local context = {}
42+
routing:access(context)
43+
44+
local mock_owner_id = 42
45+
context.route_upstream = {
46+
has_owner_id = function() return mock_owner_id end,
47+
}
48+
49+
local usage_diff = { some_metric = 1 }
50+
stub(mapping_rules_matcher, 'clean_usage_by_owner_id').returns(usage_diff)
51+
52+
local usage = { merge = function() end }
53+
stub(usage, 'merge')
54+
55+
local matched_rules = { 'rule1', 'rule2' }
56+
57+
context:route_upstream_usage_cleanup(usage, matched_rules)
58+
59+
assert.stub(mapping_rules_matcher.clean_usage_by_owner_id).was_called_with(
60+
matched_rules, mock_owner_id
61+
)
62+
assert.stub(usage.merge).was_called_with(usage, usage_diff)
63+
end)
64+
end)
65+
766
describe('.content', function()
867
describe('when there is an upstream that matches', function()
968
local upstream_that_matches = Upstream.new('http://localhost')

0 commit comments

Comments
 (0)