Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[THREESCALE-9301] Fix dns cache miss #1500

Merged
merged 6 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed Conditional policy evaluating incorrectly: second policy in policy chain that implement export() always triggers [PR #1485](https://github.com/3scale/APIcast/pull/1485) [THREESCALE-9320](https://issues.redhat.com/browse/THREESCALE-9320)
- Fix APIcast using stale configuration for deleted products [PR #1488](https://github.com/3scale/APIcast/pull/1488) [THREESCALE-10130](https://issues.redhat.com/browse/THREESCALE-10130)
- Fixed Mutual TLS between APIcast and the Backend API fails when using a Forward Proxy [PR #1499](https://github.com/3scale/APIcast/pull/1499) [THREESCALE-5105](https://issues.redhat.com/browse/THREESCALE-5105)
- Fixed dns cache miss [PR #1500](https://github.com/3scale/APIcast/pull/1500) [THEESCALE-9301](https://issues.redhat.com/browse/THREESCALE-9301)

### Added

Expand Down
111 changes: 62 additions & 49 deletions gateway/src/resty/resolver.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@
local resty_env = require 'resty.env'
local upstream = require 'ngx.upstream'
local re = require('ngx.re')
local semaphore = require "ngx.semaphore"
local semaphore = require "ngx.semaphore".new(1)
local synchronization = require('resty.synchronization').new(1)

local init = semaphore.new(1)
local table_new = require("table.new")

local default_resolver_port = 53

local _M = {
_VERSION = '0.1',
}

local TYPE_A = 1

local mt = { __index = _M }

local function read_resolv_conf(path)
Expand Down Expand Up @@ -171,14 +172,14 @@
end

function _M.nameservers()
local ok, _ = init:wait(0)
local ok, _ = semaphore:wait(0)

if ok and #(_M._nameservers) == 0 then
_M.init()
end

if ok then
init:post()
semaphore:post()
end

return _M._nameservers
Expand Down Expand Up @@ -287,65 +288,68 @@
return answers and not answers.errcode and #answers > 0 and (not answers.addresses or #answers.addresses > 0)
end

local function search_dns(self, qname, stale)
local function resolve_upstream(qname)
local peers, err = upstream.get_primary_peers(qname)

local search = self.search
local dns = self.dns
local options = self.options
local cache = self.cache
if not peers then
return nil, err
end

local function get_answer(query)
local answers, err
answers, err = cache:get(query, stale)
if valid_answers(answers) then
return answers, err
end
for i=1, #peers do
local m = re.split(peers[i].name, ':', 'oj')

Check warning on line 299 in gateway/src/resty/resolver.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/resolver.lua#L298-L299

Added lines #L298 - L299 were not covered by tests

answers, err = dns:query(query, options)
if valid_answers(answers) then
cache:save(answers)
return answers, err
end
return nil, err
peers[i] = new_answer(m[1], m[2])

Check warning on line 301 in gateway/src/resty/resolver.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/resolver.lua#L301

Added line #L301 was not covered by tests
end

return peers

Check warning on line 304 in gateway/src/resty/resolver.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/resolver.lua#L304

Added line #L304 was not covered by tests
end

-- construct search list from resolv options: search
-- @param search table of search domain
-- @param qname the name to query for
-- @return table with search names
local function search_list(search, qname)
-- FQDN
if sub(qname, -1) == "." then
local query = sub(qname, 1 ,-2)
ngx.log(ngx.DEBUG, 'resolver query: ', qname, ' query: ', query)
return get_answer(query)
return {query}
end

local answer, err
local names = table_new(#search +1, 0)
for i=1, #search do
local query = qname .. '.' .. search[i]
ngx.log(ngx.DEBUG, 'resolver query: ', qname, ' search: ', search[i], ' query: ', query)
answer, err = get_answer(query)
if answer then
return answer, err
end
names[i] = qname .. "." .. search[i]
end

return nil, err
return names
end

local function resolve_upstream(qname)
local peers, err = upstream.get_primary_peers(qname)
local function search_dns(self, qname)

if not peers then
return nil, err
end
local search = self.search
local dns = self.dns
local options = self.options
local queries = search_list(search, qname)
local answers, err

for i=1, #peers do
local m = re.split(peers[i].name, ':', 'oj')
-- Nothing found, append search domain and query DNS server
-- Return the first valid answer
for _, query in ipairs(queries) do
ngx.log(ngx.DEBUG, 'resolver query: ', qname, ' query: ', query)

peers[i] = new_answer(m[1], m[2])
answers, err = dns:query(query, options)
if valid_answers(answers) then
return answers, err
end
end

return peers
return nil, err
end


function _M.lookup(self, qname, stale)
local cache = self.cache
local options = self.options
local qtype = options.qtype or TYPE_A

ngx.log(ngx.DEBUG, 'resolver query: ', qname)

Expand All @@ -355,20 +359,28 @@
ngx.log(ngx.DEBUG, 'host is ip address: ', qname)
answers = { new_answer(qname) }
else
if is_fqdn(qname) then
answers, err = cache:get(qname, stale)
else
answers, err = resolve_upstream(qname)
local key = qname .. ":" .. qtype

-- Check cache first
answers, err = cache:get(key, stale)
if valid_answers(answers) then
return answers, nil
end

if not valid_answers(answers) then
answers, err = search_dns(self, qname, stale)
if not is_fqdn(qname) then
answers, err = resolve_upstream(qname)

if valid_answers(answers) then
return answers, nil

Check warning on line 374 in gateway/src/resty/resolver.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/resty/resolver.lua#L374

Added line #L374 was not covered by tests
end
end

answers, err = search_dns(self, qname)
if answers then
cache:save(qname, qtype, answers)
end
end

ngx.log(ngx.DEBUG, 'resolver query: ', qname, ' finished with ', #(answers or empty), ' answers')

return answers, err
end

Expand All @@ -390,6 +402,7 @@
local ok = sema:wait(0)

local answers, err = self:lookup(qname, not ok)
ngx.log(ngx.DEBUG, 'resolver query: ', qname, ' finished with ', #(answers or empty), ' answers')

if ok then
-- cleanup the key so we don't have unbounded growth of this table
Expand Down
29 changes: 22 additions & 7 deletions gateway/src/resty/resolver/cache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local _M = {
_VERSION = '0.1'
}


local mt = { __index = _M }

local shared_lrucache = resty_lrucache.new(1000)
Expand All @@ -35,17 +36,18 @@ local function compact_answers(servers)

if server then
local name = server.name or server.address
local type = server.type

local packed = hash[name]

if packed then
insert(packed, server)
packed.ttl = min(packed.ttl, server.ttl)
else
packed = {
server,
name = name,
ttl = server.ttl
ttl = server.ttl,
type = type,
}

insert(compact, packed)
Expand All @@ -57,7 +59,7 @@ local function compact_answers(servers)
return compact
end

function _M.store(self, answer, force_ttl)
function _M.store(self, qname, qtype, answer, force_ttl)
local cache = self.cache

if not cache then
Expand All @@ -71,23 +73,36 @@ function _M.store(self, answer, force_ttl)
return nil, 'invalid answer'
end

ngx.log(ngx.DEBUG, 'resolver cache write ', name, ' with TLL ', answer.ttl)
local type = answer.type

if not type then
ngx.log(ngx.WARN, 'resolver cache write refused invalid answer type ', inspect(answer))
return nil, 'invalid answer'
end

if type == qtype then
name = qname
end


local ttl = force_ttl or answer.ttl

if ttl == -1 then
ttl = nil
end

return cache:set(name, answer, ttl)
local key = name .. ":" .. qtype
ngx.log(ngx.DEBUG, 'resolver cache write ', key, ' with TLL ', ttl)

return cache:set(key, answer, ttl)
end


function _M.save(self, answers)
function _M.save(self, qname, qtype, answers)
local ans = compact_answers(answers or {})

for _, answer in pairs(ans) do
local _, err = self:store(answer)
local _, err = self:store(qname, qtype, answer)

if err then
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion script/resolver
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env sh

exec resty -I apicast/src script/resolver.lua "$@"
exec resty -I gateway/src script/resolver.lua "$@"
31 changes: 21 additions & 10 deletions spec/resty/resolver/cache_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('resty.resolver.cache', function()
it('returns compacted answers', function()
local keys = {}

for _,v in ipairs(c:save(answers)) do
for _,v in ipairs(c:save('www.example.com', 1, answers)) do
table.insert(keys, v.name)
end

Expand All @@ -51,7 +51,7 @@ describe('resty.resolver.cache', function()
it('stores the result', function()
c.store = spy.new(c.store)

c:save(answers)
c:save('eld.example.com', 1, answers)

assert.spy(c.store).was.called(3) -- TODO: proper called_with(args)
end)
Expand All @@ -63,40 +63,51 @@ describe('resty.resolver.cache', function()

it('writes to the cache', function()
local record = { 'someting' }
local answer = { record, ttl = 60, name = 'foo.example.com' }
local answer = { record, ttl = 60, name = 'foo.example.com', type = 1 }
c.cache.set = spy.new(function(_, key, value, ttl)
assert.same('foo.example.com', key)
assert.same('foo.example.com:1', key)
assert.same(answer, value)
assert.same(60, ttl)
end)

c:store(answer)
c:store('foo.example.com', 1, answer)

assert.spy(c.cache.set).was.called(1)
end)

it('works with -1 ttl', function()
local answer = { { 'something' }, ttl = -1, name = 'foo.example.com' }
local answer = { { 'something' }, ttl = -1, name = 'foo.example.com', type = 1 }

c.cache.set = spy.new(function(_, key, value, ttl)
assert.same('foo.example.com', key)
assert.same('foo.example.com:1', key)
assert.same(answer, value)
assert.same(nil, ttl)
end)

c:store(answer)
c:store('foo.example.com', 1, answer)

assert.spy(c.cache.set).was.called(1)
end)

it('return error when name is missing', function()
local answer = { { 'something' }, ttl = -1 }
c.cache.set = spy.new(function(_, key, value, ttl)
end)

local _, err = c:store('something', 1, answer)

assert.same(err, "invalid answer")
assert.spy(c.cache.set).was_not_called()
end)
end)

describe('.get', function()
local c = resolver_cache.new()

it('returns answers', function()
c:save(answers)
c:save('www.example.com', 1, answers)

local ans = c:get('www.example.com')
local ans = c:get('www.example.com:1')

assert.same({ "54.221.208.116", "54.221.221.16" }, ans.addresses)
end)
Expand Down
2 changes: 1 addition & 1 deletion spec/resty/resolver/http_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('resty.resolver.http', function()
it('resolves localhost', function()
local client = _M.new()
client:set_timeout(1000)
client.resolver.cache:save({ { address = '127.0.0.1', name = 'unknown.', ttl = 1800 } })
client.resolver.cache:save('unknown', 1, { { address = '127.0.0.1', name = 'unknown.', ttl = 1800 , type=1} })
assert(client:connect({scheme="http", host='unknown', port=1984}))
assert.equal('unknown', client.host)
assert.equal(1984, client.port)
Expand Down
2 changes: 1 addition & 1 deletion spec/resty/resolver/socket_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('resty.resolver.socket', function()
sock:settimeout(1000)
local wrapper = _M.new(sock)

wrapper.resolver.cache:save({ { address = '127.0.0.1', name = 'unknown.', ttl = 1800 } })
wrapper.resolver.cache:save('unknown', 1, { { address = '127.0.0.1', name = 'unknown.', ttl = 1800, type = 1} })
assert(wrapper:connect('unknown', 1984))
assert.equal('unknown', wrapper.host)
assert.equal(1984, wrapper.port)
Expand Down
Loading
Loading