Skip to content

Commit

Permalink
Async lookups (#15)
Browse files Browse the repository at this point in the history
* wip(cache) implemented LRU cache

a number of tests marked as pending waiting for updating to the new
async/stale query method

* wip(client) implements async dns resolution.

rewrites the core resolution code
  • Loading branch information
Tieske authored Jun 14, 2017
1 parent 39c22f0 commit c3afa56
Show file tree
Hide file tree
Showing 6 changed files with 968 additions and 600 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ use the `rbusted` script.
History
=======

### 0.5.x (xx-xxx-2017) Bugfixes
### 0.6.x (xx-xxx-2017) Rewritten resolver core to resolve async

- Added: resolution will be done async whenever possible. For this to work a new
setting has been introduced `staleTtl` which determines for how long stale
records will returned while a query is in progress in the background.
- Change: BREAKING! several functions that previously returned and took a
resolver object no longer do so.
- Fix: no longer lookup ip adresses as names if the query type is not A or AAAA
- Fix: normalize names to lowercase after query
- Fix: set last-success types for hosts-file entries and ip-addresses
Expand Down
29 changes: 19 additions & 10 deletions spec/balancer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ else
end

-- creates an SRV record in the cache
local dnsSRV = function(records)
local dnsSRV = function(records, staleTtl)
-- if single table, then insert into a new list
if not records[1] then records = { records } end

Expand All @@ -43,14 +43,15 @@ local dnsSRV = function(records)
records.expire = gettime() + records[1].ttl

-- create key, and insert it
dnscache[records[1].type..":"..records[1].name] = records
local key = records[1].type..":"..records[1].name
dnscache:set(key, records, records[1].ttl + (staleTtl or 4))
-- insert last-succesful lookup type
dnscache[records[1].name] = records[1].type
dnscache:set(records[1].name, records[1].type)
return records
end

-- creates an A record in the cache
local dnsA = function(records)
local dnsA = function(records, staleTtl)
-- if single table, then insert into a new list
if not records[1] then records = { records } end

Expand All @@ -71,14 +72,15 @@ local dnsA = function(records)
records.expire = gettime() + records[1].ttl

-- create key, and insert it
dnscache[records[1].type..":"..records[1].name] = records
local key = records[1].type..":"..records[1].name
dnscache:set(key, records, records[1].ttl + (staleTtl or 4))
-- insert last-succesful lookup type
dnscache[records[1].name] = records[1].type
dnscache:set(records[1].name, records[1].type)
return records
end

-- creates an AAAA record in the cache
local dnsAAAA = function(records)
local dnsAAAA = function(records, staleTtl)
-- if single table, then insert into a new list
if not records[1] then records = { records } end

Expand All @@ -99,9 +101,10 @@ local dnsAAAA = function(records)
records.expire = gettime() + records[1].ttl

-- create key, and insert it
dnscache[records[1].type..":"..records[1].name] = records
local key = records[1].type..":"..records[1].name
dnscache:set(key, records, records[1].ttl + (staleTtl or 4))
-- insert last-succesful lookup type
dnscache[records[1].name] = records[1].type
dnscache:set(records[1].name, records[1].type)
return records
end

Expand Down Expand Up @@ -1023,7 +1026,13 @@ describe("Loadbalancer", function()
["[::1]:80"] = 30,
}, count)

record.expire = gettime() - 1 -- expire record now
-- expire the existing record
record.expire = gettime() - 1
record.expired = true
-- do a lookup to trigger the async lookup
client.resolve("does.not.exist.mashape.com", {qtype = client.TYPE_A})
sleep(0.5) -- provide time for async lookup to complete

for _ = 1, b.wheelSize do b:getPeer() end -- hit them all to force renewal

count = count_slots(b)
Expand Down
227 changes: 227 additions & 0 deletions spec/client_cache_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
local pretty = require("pl.pretty").write
local _

-- empty records and not found errors should be identical, hence we
-- define a constant for that error message
local NOT_FOUND_ERROR = "dns server error: 3 name error"

local gettime, sleep
if ngx then
gettime = ngx.now
sleep = ngx.sleep
else
local socket = require("socket")
gettime = socket.gettime
sleep = socket.sleep
end

-- simple debug function
local dump = function(...)
print(require("pl.pretty").write({...}))
end

describe("DNS client cache", function()

local client, resolver, query_func

before_each(function()
_G._TEST = true
client = require("resty.dns.client")
resolver = require("resty.dns.resolver")

-- you can replace this `query_func` upvalue to spy on resolver query calls.
-- This default will just call the original resolver (hence is transparent)
query_func = function(self, original_query_func, name, options)
return original_query_func(self, name, options)
end

-- patch the resolver lib, such that any new resolver created will query
-- using the `query_func` upvalue defined above
local old_new = resolver.new
resolver.new = function(...)
local r = old_new(...)
local original_query_func = r.query
r.query = function(self, ...)
if not query_func then
print(debug.traceback("WARNING: query_func is not set"))
dump(self, ...)
return
end
return query_func(self, original_query_func, ...)
end
return r
end
end)

after_each(function()
package.loaded["resty.dns.client"] = nil
package.loaded["resty.dns.resolver"] = nil
client = nil
resolver = nil
query_func = nil
_G._TEST = nil
end)

describe("shortnames", function()

local lrucache, mock_records, config
before_each(function()
config = {
nameservers = { "8.8.8.8" },
ndots = 1,
search = { "domain.com" },
hosts = {},
resolvConf = {},
order = { "LAST", "SRV", "A", "AAAA", "CNAME" },
badTtl = 0.5,
staleTtl = 0.5,
enable_ipv6 = false,
}
assert(client.init(config))
lrucache = client.getcache()

query_func = function(self, original_query_func, qname, opts)
return mock_records[qname..":"..opts.qtype] or { errcode = 3, errstr = "name error" }
end
end)

it("are stored in cache without type", function()
mock_records = {
["myhost1.domain.com:"..client.TYPE_A] = {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "myhost1.domain.com",
ttl = 30,
}}
}

local result = client.resolve("myhost1")
assert.equal(result, lrucache:get("none:short:myhost1"))
end)

it("are stored in cache with type", function()
mock_records = {
["myhost2.domain.com:"..client.TYPE_A] = {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "myhost2.domain.com",
ttl = 30,
}}
}

local result = client.resolve("myhost2", { qtype = client.TYPE_A })
assert.equal(result, lrucache:get(client.TYPE_A..":short:myhost2"))
end)

it("are resolved from cache without type", function()
mock_records = {}
lrucache:set("none:short:myhost3", {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "myhost3.domain.com",
ttl = 30,
},
ttl = 30,
expire = gettime() + 30,
}, 30+4)

local result = client.resolve("myhost3")
assert.equal(result, lrucache:get("none:short:myhost3"))
end)

it("are resolved from cache with type", function()
mock_records = {}
lrucache:set(client.TYPE_A..":short:myhost4", {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "myhost4.domain.com",
ttl = 30,
},
ttl = 30,
expire = gettime() + 30,
}, 30+4)

local result = client.resolve("myhost4", { qtype = client.TYPE_A })
assert.equal(result, lrucache:get(client.TYPE_A..":short:myhost4"))
end)

it("of dereferenced CNAME are stored in cache", function()
mock_records = {
["myhost5.domain.com:"..client.TYPE_CNAME] = {{
type = client.TYPE_CNAME,
class = 1,
name = "myhost5.domain.com",
cname = "mytarget.domain.com",
ttl = 30,
}},
["mytarget.domain.com:"..client.TYPE_A] = {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "mytarget.domain.com",
ttl = 30,
}}
}
local result = client.resolve("myhost5")

assert.same(mock_records["mytarget.domain.com:"..client.TYPE_A], result) -- not the test, intermediate validation

-- the type un-specificc query was the CNAME, so that should be in the
-- shorname cache
assert.same(mock_records["myhost5.domain.com:"..client.TYPE_CNAME],
lrucache:get("none:short:myhost5"))
end)

it("ttl in cache is honored for short name entries", function()
-- in the short name case the same record is inserted again in the cache
-- and the lru-ttl has to be calculated, make sure it is correct
mock_records = {
["myhost6.domain.com:"..client.TYPE_A] = {{
type = client.TYPE_A,
address = "1.2.3.4",
class = 1,
name = "myhost6.domain.com",
ttl = 0.1,
}}
}
local mock_copy = require("pl.tablex").deepcopy(mock_records)

-- resolve and check whether we got the mocked record
local result = client.resolve("myhost6")
assert.equal(result, mock_records["myhost6.domain.com:"..client.TYPE_A])

-- replace our mocked list with the copy made (new table, so no equality)
mock_records = mock_copy

-- wait for expiring
sleep(0.1 + config.staleTtl / 2)

-- resolve again, now getting same record, but stale, this will trigger
-- background refresh query
local result2 = client.resolve("myhost6")
assert.equal(result2, result)
assert.is_true(result2.expired) -- stale; marked as expired

-- wait for refresh to complete
sleep(0.1)

-- resolve and check whether we got the new record from the mock copy
local result3 = client.resolve("myhost6")
assert.not_equal(result, result3) -- must be a different record now
assert.equal(result3, mock_records["myhost6.domain.com:"..client.TYPE_A])

-- the 'result3' resolve call above will also trigger a new background query
-- (because the sleep of 0.1 equals the records ttl of 0.1)
-- so let's yield to activate that background thread now. If not done so,
-- the `after_each` will clear `query_func` and an error will appear on the
-- next test after this one that will yield.
sleep(0.1)
end)

end)

end)
Loading

0 comments on commit c3afa56

Please sign in to comment.