
assert:set_parameter("TableFormatLevel", 5) -- when displaying tables, set a bigger default depth

------------------------
-- START TEST HELPERS --
------------------------
local client
local targets, balancers

local dns_utils = require "kong.resty.dns.utils"
local mocker = require "spec.fixtures.mocker"
local utils = require "kong.tools.utils"

local ws_id = utils.uuid()

local helpers = require "spec.helpers.dns"
local gettime = helpers.gettime
local sleep = helpers.sleep
local dnsSRV = function(...) return helpers.dnsSRV(client, ...) end
local dnsA = function(...) return helpers.dnsA(client, ...) end
local dnsAAAA = function(...) return helpers.dnsAAAA(client, ...) end


local unset_register = {}
local function setup_block(consistency)
  local cache_table = {}

  local function mock_cache()
    return {
      safe_set = function(self, k, v)
        cache_table[k] = v
        return true
      end,
      get = function(self, k, _, fn, arg)
        if cache_table[k] == nil then
          cache_table[k] = fn(arg)
        end
        return cache_table[k]
      end,
    }
  end

  local function register_unsettter(f)
    table.insert(unset_register, f)
  end

  mocker.setup(register_unsettter, {
    kong = {
      configuration = {
        worker_consistency = consistency,
        worker_state_update_frequency = 0.1,
      },
      core_cache = mock_cache(cache_table),
    },
    ngx = {
      ctx = {
        workspace = ws_id,
      }
    }
  })
end

local function unsetup_block()
  for _, f in ipairs(unset_register) do
    f()
  end
end



local function insert_target(b, name, port, weight)
  -- adding again changes weight
  for _, prev_target in ipairs(b.targets) do
    if prev_target.name == name and prev_target.port == port then
      local entry = {port = port}
      for _, addr in ipairs(prev_target.addresses) do
        entry.address = addr.ip
        b:changeWeight(prev_target, entry, weight)
      end
      prev_target.weight = weight
      return prev_target
    end
  end

  if type(name) == "table" then
    local entry = name
    name = entry.name or entry[1]
    port = entry.port or entry[2]
    weight = entry.weight or entry[3]
  end

  local target = {
    upstream = b.upstream_id,
    balancer = b,
    name = name,
    nameType = dns_utils.hostnameType(name),
    addresses = {},
    port = port or 80,
    weight = weight or 100,
    totalWeight = 0,
    unavailableWeight = 0,
  }
  table.insert(b.targets, target)

  return target
end

local function add_target(b, ...)
  local target = insert_target(b, ...)
  targets.resolve_targets(b.targets)
  return target
end


local upstream_index = 0
local function new_balancer(opts)
  upstream_index = upstream_index + 1
  local upname="upstream_" .. upstream_index
  local hc_defaults = {
    active = {
      timeout = 1,
      concurrency = 10,
      http_path = "/",
      healthy = {
        interval = 0,  -- 0 = probing disabled by default
        http_statuses = { 200, 302 },
        successes = 0, -- 0 = disabled by default
      },
      unhealthy = {
        interval = 0, -- 0 = probing disabled by default
        http_statuses = { 429, 404,
                          500, 501, 502, 503, 504, 505 },
        tcp_failures = 0,  -- 0 = disabled by default
        timeouts = 0,      -- 0 = disabled by default
        http_failures = 0, -- 0 = disabled by default
      },
    },
    passive = {
      healthy = {
        http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
                          300, 301, 302, 303, 304, 305, 306, 307, 308 },
        successes = 0,
      },
      unhealthy = {
        http_statuses = { 429, 500, 503 },
        tcp_failures = 0,  -- 0 = circuit-breaker disabled by default
        timeouts = 0,      -- 0 = circuit-breaker disabled by default
        http_failures = 0, -- 0 = circuit-breaker disabled by default
      },
    },
  }
  local my_upstream = { id=upname, name=upname, ws_id=ws_id, slots=10, healthchecks=hc_defaults, algorithm="round-robin" }
  local b = (balancers.create_balancer(my_upstream, true))

  for k, v in pairs{
    wheelSize = opts.wheelSize,
    requeryInterval = opts.requery,
    ttl0Interval = opts.ttl0,
  } do
    b[k] = v
  end

  if opts.callback then
    b:setCallback(opts.callback)
  end

  for _, target in ipairs(opts.hosts or {}) do
    insert_target(b, target)
  end
  targets.resolve_targets(b.targets)

  return b
end



-- checks the integrity of a list, returns the length of list + number of non-array keys
local check_list = function(t)
  local size = 0
  local keys = 0
  for i, _ in pairs(t) do
    if (type(i) == "number") then
      if (i > size) then size = i end
    else
      keys = keys + 1
    end
  end
  for i = 1, size do
    assert(t[i], "invalid sequence, index "..tostring(i).." is missing")
  end
  return size, keys
end

-- checks the integrity of the balancer, hosts, addresses, and indices. returns the balancer.
local check_balancer = function(b)
  assert.is.table(b)
  assert.is.table(b.algorithm)
  check_list(b.targets)
  assert.are.equal(b.algorithm.wheelSize, check_list(b.algorithm.wheel))
  return b
end

-- creates a hash table with "address:port" keys and as value the number of indices
local function count_indices(b)
  local r = {}
  for _, address in ipairs(b.algorithm.wheel) do
    local key = tostring(address.ip)
    if key:find(":",1,true) then
      key = "["..key.."]:"..address.port
    else
      key = key..":"..address.port
    end
    r[key] = (r[key] or 0) + 1
  end
  return r
end

-- copies the wheel to a list with ip, port and hostname in the field values.
-- can be used for before/after comparison
local copyWheel = function(b)
  local copy = {}
  for i, address in ipairs(b.algorithm.wheel) do
    copy[i] = i.." - "..address.ip.." @ "..address.port.." ("..address.target.name..")"
  end
  return copy
end

local updateWheelState = function(state, patt, repl)
  for i, entry in ipairs(state) do
    state[i] = entry:gsub(patt, repl, 1)
  end
  return state
end
----------------------
-- END TEST HELPERS --
----------------------


describe("[round robin balancer]", function()

  local snapshot

  setup(function()
    _G.package.loaded["kong.resty.dns.client"] = nil -- make sure module is reloaded
    _G.package.loaded["kong.runloop.balancer.targets"] = nil -- make sure module is reloaded

    client = require "kong.resty.dns.client"
    targets = require "kong.runloop.balancer.targets"
    balancers = require "kong.runloop.balancer.balancers"
    local healthcheckers = require "kong.runloop.balancer.healthcheckers"
    healthcheckers.init()
    balancers.init()

    local singletons = require "kong.singletons"
    singletons.worker_events = require "resty.worker.events"
    singletons.worker_events.configure({
      shm = "kong_process_events", -- defined by "lua_shared_dict"
      timeout = 5,            -- life time of event data in shm
      interval = 1,           -- poll interval (seconds)

      wait_interval = 0.010,  -- wait before retry fetching event data
      wait_max = 0.5,         -- max wait time before discarding event
    })

    local function empty_each()
      return function() end
    end

    singletons.db = {
      targets = {
        each = empty_each,
        select_by_upstream_raw = function()
          return {}
        end
      },
      upstreams = {
        each = empty_each,
        select = function() end,
      },
    }

    singletons.core_cache = {
      _cache = {},
      get = function(self, key, _, loader, arg)
        local v = self._cache[key]
        if v == nil then
          v = loader(arg)
          self._cache[key] = v
        end
        return v
      end,
      invalidate_local = function(self, key)
        self._cache[key] = nil
      end
    }

  end)

  before_each(function()
    setup_block()
    assert(client.init {
      hosts = {},
      resolvConf = {
        "nameserver 8.8.8.8"
      },
    })
    snapshot = assert:snapshot()
  end)

  after_each(function()
    unsetup_block()
    snapshot:revert()  -- undo any spying/stubbing etc.
    collectgarbage()
    collectgarbage()
  end)

  describe("unit tests", function()
    it("addressIter", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsAAAA({
        { name = "getkong.test", address = "::1" },
      })
      dnsSRV({
        { name = "gelato.test", target = "1.2.3.6", port = 8001 },
        { name = "gelato.test", target = "1.2.3.6", port = 8002 },
        { name = "gelato.test", target = "1.2.3.6", port = 8003 },
      })
      local b = new_balancer{
        hosts = {"mashape.test", "getkong.test", "gelato.test" },
        dns = client,
        wheelSize = 10,
      }
      local count = 0
      --for _,_,_ in b:addressIter() do count = count + 1 end
      b:eachAddress(function() count = count + 1  end)
      assert.equals(6, count)
    end)

    describe("create", function()
      it("succeeds with proper options", function()
        dnsA({
          { name = "mashape.test", address = "1.2.3.4" },
          { name = "mashape.test", address = "1.2.3.5" },
        })
        check_balancer(new_balancer{
          hosts = {"mashape.test"},
          dns = client,
          requery = 2,
          ttl0 = 5,
          callback = function() end,
        })
      end)
      it("succeeds without 'hosts' option", function()
        local b = check_balancer(new_balancer{
          dns = client,
        })
        assert.are.equal(0, #b.algorithm.wheel)

        b = check_balancer(new_balancer{
          dns = client,
          hosts = {},  -- empty hosts table hould work too
        })
        assert.are.equal(0, #b.algorithm.wheel)
      end)
      it("succeeds with multiple hosts", function()
        dnsA({
          { name = "mashape.test", address = "1.2.3.4" },
        })
        dnsAAAA({
          { name = "getkong.test", address = "::1" },
        })
        dnsSRV({
          { name = "gelato.test", target = "1.2.3.4", port = 8001 },
        })
        local b = new_balancer{
          hosts = {"mashape.test", "getkong.test", "gelato.test" },
          dns = client,
          wheelSize = 10,
        }
        check_balancer(b)
      end)
    end)

    describe("adding hosts", function()
      it("accepts a hostname that does not resolve", function()
        -- weight should be 0, with no addresses
        local b = check_balancer(new_balancer {
          dns = client,
          wheelSize = 15,
        })
        assert(add_target(b, "really.really.really.does.not.exist.hostname.test", 80, 10))
        check_balancer(b)
        assert.equals(0, b.totalWeight) -- has one failed host, so weight must be 0
        dnsA({
          { name = "mashape.test", address = "1.2.3.4" },
        })
        add_target(b, "mashape.test", 80, 10)
        check_balancer(b)
        assert.equals(10, b.totalWeight) -- has one succesful host, so weight must equal that one
      end)
      it("accepts a hostname when dns server is unavailable #slow", function()
        -- This test might show some error output similar to the lines below. This is expected and ok.
        -- 2016/11/07 16:48:33 [error] 81932#0: *2 recv() failed (61: Connection refused), context: ngx.timer

        -- reconfigure the dns client to make sure query fails
        assert(client.init {
          hosts = {},
          resolvConf = {
            "nameserver 127.0.0.1:22000" -- make sure dns query fails
          },
        })
        -- create balancer
        local b = check_balancer(new_balancer {
         requery = 0.1,
         hosts = {
            { name = "mashape.test", port = 80, weight = 10 },
          },
          dns = client,
        })
        assert.equal(0, b.totalWeight)
      end)
      it("updates the weight when 'hostname:port' combo already exists", function()
        -- returns nil + error
        local b = check_balancer(new_balancer {
          dns = client,
          wheelSize = 15,
        })
        dnsA({
          { name = "mashape.test", address = "1.2.3.4" },
        })
        add_target(b, "mashape.test", 80, 10)
        check_balancer(b)
        assert.equal(10, b.totalWeight)

        add_target(b, "mashape.test", 81, 20)  -- different port
        check_balancer(b)
        assert.equal(30, b.totalWeight)

        add_target(b, "mashape.test", 80, 5)  -- reduce weight by 5
        check_balancer(b)
        assert.equal(25, b.totalWeight)
      end)
    end)

    describe("setting status", function()
      it("valid target is accepted", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "kong.inc", address = "4.3.2.1" },
        })
        add_target(b, "1.2.3.4", 80, 10)
        add_target(b, "kong.inc", 80, 10)
        --local ok, err = b:setAddressStatus(false, "1.2.3.4", 80, "1.2.3.4")
        local ok, err = b:setAddressStatus(b:findAddress("1.2.3.4", 80, "1.2.3.4"), false)
        assert.is_true(ok)
        assert.is_nil(err)
        ok, err = b:setAddressStatus(b:findAddress("4.3.2.1", 80, "kong.inc"), false)
        assert.is_true(ok)
        assert.is_nil(err)
      end)
      it("valid address accepted", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "kong.inc", address = "4.3.2.1" },
        })
        add_target(b, "kong.inc", 80, 10)
        local _, _, _, handle = b:getPeer()
        local ok, err = b:setAddressStatus(handle.address, false)
        assert.is_true(ok)
        assert.is_nil(err)
      end)
      it("invalid target returns an error", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "kong.inc", address = "4.3.2.1" },
        })
        add_target(b, "1.2.3.4", 80, 10)
        add_target(b, "kong.inc", 80, 10)

        --local ok, err = b:setAddressStatus(false, "1.1.1.1", 80)
        local ok, err = b:setAddressStatus(b:findAddress("1.1.1.1", 80), false)
        assert.is_nil(ok)
        --assert.equals("no peer found by name '1.1.1.1' and address 1.1.1.1:80", err)
        assert.is_string(err)
        ok, err = b:setAddressStatus(b:findAddress("1.1.1.1", 80, "kong.inc"), false)
        assert.is_nil(ok)
        --assert.equals("no peer found by name 'kong.inc' and address 1.1.1.1:80", err)
        assert.is_string(err)
      end)
      it("SRV target with A record targets can be changed with a handle", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "mashape1.test", address = "12.34.56.1" },
        })
        dnsA({
          { name = "mashape2.test", address = "12.34.56.2" },
        })
        dnsSRV({
          { name = "mashape.test", target = "mashape1.test", port = 8001, weight = 5 },
          { name = "mashape.test", target = "mashape2.test", port = 8002, weight = 5 },
        })
        add_target(b, "mashape.test", 80, 10)

        local _, _, _, handle = b:getPeer()
        local ok, err = b:setAddressStatus(handle.address, false)
        assert.is_true(ok)
        assert.is_nil(err)

        _, _, _, handle = b:getPeer()
        ok, err = b:setAddressStatus(handle.address, false)
        assert.is_true(ok)
        assert.is_nil(err)

        local ip, port = b:getPeer()
        assert.is_nil(ip)
        assert.matches("Balancer is unhealthy", port)

      end)
      it("SRV target with A record targets can be changed with an address", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "mashape1.test", address = "12.34.56.1" },
        })
        dnsA({
          { name = "mashape2.test", address = "12.34.56.2" },
        })
        dnsSRV({
          { name = "mashape.test", target = "mashape1.test", port = 8001, weight = 5 },
          { name = "mashape.test", target = "mashape2.test", port = 8002, weight = 5 },
        })
        add_target(b, "mashape.test", 80, 10)

        local _, _, _, handle = b:getPeer()
        local ok, err = b:setAddressStatus(handle.address, false)
        assert.is_true(ok)
        assert.is_nil(err)

        _, _, _, handle = b:getPeer()
        ok, err = b:setAddressStatus(handle.address, false)
        assert.is_true(ok)
        assert.is_nil(err)

        local ip, port = b:getPeer()
        assert.is_nil(ip)
        assert.matches("Balancer is unhealthy", port)

      end)
      it("SRV target with port=0 returns the default port", function()
        local b = check_balancer(new_balancer { dns = client })
        dnsA({
          { name = "mashape1.test", address = "12.34.56.78" },
        })
        dnsSRV({
          { name = "mashape.test", target = "mashape1.test", port = 0, weight = 5 },
        })
        add_target(b, "mashape.test", 80, 10)
        local ip, port = b:getPeer()
        assert.equals("12.34.56.78", ip)
        assert.equals(80, port)
      end)
    end)

  end)

  describe("getting targets", function()
    it("gets an IP address, port and hostname for named SRV entries", function()
      -- this case is special because it does a last-minute `toip` call and hence
      -- uses a different code branch
      -- See issue #17
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsSRV({
        { name = "gelato.test", target = "mashape.test", port = 8001 },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          {name = "gelato.test", port = 123, weight = 100},
        },
        dns = client,
      })
      local addr, port, host = b:getPeer()
      assert.equal("1.2.3.4", addr)
      assert.equal(8001, port)
      assert.equal("gelato.test", host)
    end)
    it("gets an IP address and port number; round-robin", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsA({
        { name = "getkong.test", address = "5.6.7.8" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          {name = "mashape.test", port = 123, weight = 100},
          {name = "getkong.test", port = 321, weight = 50},
        },
        dns = client,
      })
      -- run down the wheel twice
      local res = {}
      for _ = 1, 15*2 do
        local addr, port, host = b:getPeer()
        res[addr..":"..port] = (res[addr..":"..port] or 0) + 1
        res[host..":"..port] = (res[host..":"..port] or 0) + 1
      end
      assert.equal(20, res["1.2.3.4:123"])
      assert.equal(20, res["mashape.test:123"])
      assert.equal(10, res["5.6.7.8:321"])
      assert.equal(10, res["getkong.test:321"])
    end)
    it("gets an IP address and port number; round-robin skips unhealthy addresses", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsA({
        { name = "getkong.test", address = "5.6.7.8" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          {name = "mashape.test", port = 123, weight = 100},
          {name = "getkong.test", port = 321, weight = 50},
        },
        dns = client,
        wheelSize = 15,
      })
      -- mark node down
      assert(b:setAddressStatus(b:findAddress("1.2.3.4", 123, "mashape.test"), false))
      -- run down the wheel twice
      local res = {}
      for _ = 1, 15*2 do
        local addr, port, host = b:getPeer()
        res[addr..":"..port] = (res[addr..":"..port] or 0) + 1
        res[host..":"..port] = (res[host..":"..port] or 0) + 1
      end
      assert.equal(nil, res["1.2.3.4:123"])     -- address got no hits, key never gets initialized
      assert.equal(nil, res["mashape.test:123"]) -- host got no hits, key never gets initialized
      assert.equal(30, res["5.6.7.8:321"])
      assert.equal(30, res["getkong.test:321"])
    end)
    it("does not hit the resolver when 'cache_only' is set", function()
      local record = dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      local b = check_balancer(new_balancer {
        hosts = { { name = "mashape.test", port = 80, weight = 5 } },
        dns = client,
        wheelSize = 10,
      })
      record.expire = gettime() - 1 -- expire current dns cache record
      dnsA({   -- create a new record
        { name = "mashape.test", address = "5.6.7.8" },
      })
      -- create a spy to check whether dns was queried
      spy.on(client, "resolve")
      local hash = nil
      local cache_only = true
      local ip, port, host = b:getPeer(cache_only, nil, hash)
      assert.spy(client.resolve).Not.called_with("mashape.test",nil, nil)
      assert.equal("1.2.3.4", ip)  -- initial un-updated ip address
      assert.equal(80, port)
      assert.equal("mashape.test", host)
    end)
  end)

  describe("setting status triggers address-callback", function()
    it("for IP addresses", function()
      local count_add = 0
      local count_remove = 0
      local b
      b = check_balancer(new_balancer {
        hosts = {},  -- no hosts, so balancer is empty
        dns = client,
        wheelSize = 10,
        callback = function(balancer, action, address, ip, port, hostname)
          assert.equal(b, balancer)
          if action == "added" then
            count_add = count_add + 1
          elseif action == "removed" then
            count_remove = count_remove + 1
          elseif action == "health" then  --luacheck: ignore
            -- nothing to do
          else
            error("unknown action received: "..tostring(action))
          end
          if action ~= "health" then
            assert.equals("12.34.56.78", ip)
            assert.equals(123, port)
            assert.equals("12.34.56.78", hostname)
          end
        end
      })
      add_target(b, "12.34.56.78", 123, 100)
      ngx.sleep(0)
      assert.equal(1, count_add)
      assert.equal(0, count_remove)

      --b:removeHost("12.34.56.78", 123)
      b.targets[1].addresses[1].disabled = true
      b:deleteDisabledAddresses(b.targets[1])
      ngx.sleep(0)
      assert.equal(1, count_add)
      assert.equal(1, count_remove)
    end)
    it("for 1 level dns", function()
      local count_add = 0
      local count_remove = 0
      local b
      b = check_balancer(new_balancer {
        hosts = {},  -- no hosts, so balancer is empty
        dns = client,
        wheelSize = 10,
        callback = function(balancer, action, address, ip, port, hostname)
          assert.equal(b, balancer)
          if action == "added" then
            count_add = count_add + 1
          elseif action == "removed" then
            count_remove = count_remove + 1
          elseif action == "health" then  --luacheck: ignore
            -- nothing to do
          else
            error("unknown action received: "..tostring(action))
          end
          if action ~= "health" then
            assert.equals("12.34.56.78", ip)
            assert.equals(123, port)
            assert.equals("mashape.test", hostname)
          end
        end
      })
      dnsA({
        { name = "mashape.test", address = "12.34.56.78" },
        { name = "mashape.test", address = "12.34.56.78" },
      })
      add_target(b, "mashape.test", 123, 100)
      ngx.sleep(0)
      assert.equal(2, count_add)
      assert.equal(0, count_remove)

      b.targets[1].addresses[1].disabled = true
      b.targets[1].addresses[2].disabled = true
      b:deleteDisabledAddresses(b.targets[1])
      ngx.sleep(0)
      assert.equal(2, count_add)
      assert.equal(2, count_remove)
    end)
    it("for 2+ level dns", function()
      local count_add = 0
      local count_remove = 0
      local b
      b = check_balancer(new_balancer {
        hosts = {},  -- no hosts, so balancer is empty
        dns = client,
        wheelSize = 10,
        callback = function(balancer, action, address, ip, port, hostname)
          assert.equal(b, balancer)
          if action == "added" then
            count_add = count_add + 1
          elseif action == "removed" then
            count_remove = count_remove + 1
          elseif action == "health" then  --luacheck: ignore
            -- nothing to do
          else
            error("unknown action received: "..tostring(action))
          end
          if action ~= "health" then
            assert(ip == "mashape1.test" or ip == "mashape2.test")
            assert(port == 8001 or port == 8002)
            assert.equals("mashape.test", hostname)
          end
        end
      })
      dnsA({
        { name = "mashape1.test", address = "12.34.56.1" },
      })
      dnsA({
        { name = "mashape2.test", address = "12.34.56.2" },
      })
      dnsSRV({
        { name = "mashape.test", target = "mashape1.test", port = 8001, weight = 5 },
        { name = "mashape.test", target = "mashape2.test", port = 8002, weight = 5 },
      })
      add_target(b, "mashape.test", 123, 100)
      ngx.sleep(0)
      assert.equal(2, count_add)
      assert.equal(0, count_remove)

      --b:removeHost("mashape.test", 123)
      b.targets[1].addresses[1].disabled = true
      b.targets[1].addresses[2].disabled = true
      b:deleteDisabledAddresses(b.targets[1])
      ngx.sleep(0)
      assert.equal(2, count_add)
      assert.equal(2, count_remove)
    end)
  end)

  describe("wheel manipulation", function()
    it("wheel updates are atomic", function()
      -- testcase for issue #49, see:
      -- https://github.test/Kong/lua-resty-dns-client/issues/49
      local order_of_events = {}
      local b
      b = check_balancer(new_balancer {
        hosts = {},  -- no hosts, so balancer is empty
        dns = client,
        wheelSize = 10,
        callback = function(balancer, action, ip, port, hostname)
          table.insert(order_of_events, "callback")
          -- this callback is called when updating. So yield here and
          -- verify that the second thread does not interfere with
          -- the first update, yielded here.
          ngx.sleep(0)
        end
      })
      dnsA({
        { name = "mashape1.test", address = "12.34.56.78" },
      })
      dnsA({
        { name = "mashape2.test", address = "123.45.67.89" },
      })
      local t1 = ngx.thread.spawn(function()
        table.insert(order_of_events, "thread1 start")
        add_target(b, "mashape1.test")
        table.insert(order_of_events, "thread1 end")
      end)
      local t2 = ngx.thread.spawn(function()
        table.insert(order_of_events, "thread2 start")
        add_target(b, "mashape2.test")
        table.insert(order_of_events, "thread2 end")
      end)
      ngx.thread.wait(t1)
      ngx.thread.wait(t2)
      ngx.sleep(0)
      assert.same({
        [1] = 'thread1 start',
        [2] = 'thread1 end',
        [3] = 'thread2 start',
        [4] = 'thread2 end',
        [5] = 'callback',
        [6] = 'callback',
        [7] = 'callback',
      }, order_of_events)
    end)
    it("equal weights and 'fitting' indices", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      local b = check_balancer(new_balancer {
        hosts = {"mashape.test"},
        dns = client,
      })
      local expected = {
        ["1.2.3.4:80"] = 1,
        ["1.2.3.5:80"] = 1,
      }
      assert.are.same(expected, count_indices(b))
    end)
    it("DNS record order has no effect", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.1" },
        { name = "mashape.test", address = "1.2.3.2" },
        { name = "mashape.test", address = "1.2.3.3" },
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
        { name = "mashape.test", address = "1.2.3.6" },
        { name = "mashape.test", address = "1.2.3.7" },
        { name = "mashape.test", address = "1.2.3.8" },
        { name = "mashape.test", address = "1.2.3.9" },
        { name = "mashape.test", address = "1.2.3.10" },
      })
      local b = check_balancer(new_balancer {
        hosts = {"mashape.test"},
        dns = client,
        wheelSize = 19,
      })
      local expected = count_indices(b)
      dnsA({
        { name = "mashape.test", address = "1.2.3.8" },
        { name = "mashape.test", address = "1.2.3.3" },
        { name = "mashape.test", address = "1.2.3.1" },
        { name = "mashape.test", address = "1.2.3.2" },
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
        { name = "mashape.test", address = "1.2.3.6" },
        { name = "mashape.test", address = "1.2.3.9" },
        { name = "mashape.test", address = "1.2.3.10" },
        { name = "mashape.test", address = "1.2.3.7" },
      })
      b = check_balancer(new_balancer {
        hosts = {"mashape.test"},
        dns = client,
        wheelSize = 19,
      })

      assert.are.same(expected, count_indices(b))
    end)
    it("changing hostname order has no effect", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.1" },
      })
      dnsA({
        { name = "getkong.test", address = "1.2.3.2" },
      })
      local b = new_balancer {
        hosts = {"mashape.test", "getkong.test"},
        dns = client,
        wheelSize = 3,
      }
      local expected = count_indices(b)
      b = check_balancer(new_balancer {
        hosts = {"getkong.test", "mashape.test"},  -- changed host order
        dns = client,
        wheelSize = 3,
      })
      assert.are.same(expected, count_indices(b))
    end)
    it("adding a host (fitting indices)", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsAAAA({
        { name = "getkong.test", address = "::1" },
      })
      local b = check_balancer(new_balancer {
        hosts = { { name = "mashape.test", port = 80, weight = 5 } },
        dns = client,
      })
      add_target(b, "getkong.test", 8080, 10 )
      check_balancer(b)
      local expected = {
        ["1.2.3.4:80"] = 1,
        ["1.2.3.5:80"] = 1,
        ["[::1]:8080"] = 2,
      }
      assert.are.same(expected, count_indices(b))
    end)
    it("removing the last host", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsAAAA({
        { name = "getkong.test", address = "::1" },
      })
      local b = check_balancer(new_balancer {
        dns = client,
        wheelSize = 20,
      })
      add_target(b, "mashape.test", 80, 5)
      add_target(b, "getkong.test", 8080, 10)
      --b:removeHost("getkong.test", 8080)
      --b:removeHost("mashape.test", 80)
    end)
    it("weight change updates properly", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsAAAA({
        { name = "getkong.test", address = "::1" },
      })
      local b = check_balancer(new_balancer {
        dns = client,
        wheelSize = 60,
      })
      add_target(b, "mashape.test", 80, 10)
      add_target(b, "getkong.test", 80, 10)
      local count = count_indices(b)
      assert.same({
        ["1.2.3.4:80"] = 1,
        ["1.2.3.5:80"] = 1,
        ["[::1]:80"]   = 1,
      }, count)

      add_target(b, "mashape.test", 80, 25)
      count = count_indices(b)
      assert.same({
        ["1.2.3.4:80"] = 5,
        ["1.2.3.5:80"] = 5,
        ["[::1]:80"]   = 2,
      }, count)
    end)
    it("weight change ttl=0 record, updates properly", function()
      -- mock the resolve/toip methods
      local old_resolve = client.resolve
      local old_toip = client.toip
      finally(function()
        client.resolve = old_resolve
        client.toip = old_toip
      end)
      client.resolve = function(name, ...)
        if name == "mashape.test" then
          local record = dnsA({
            { name = "mashape.test", address = "1.2.3.4", ttl = 0 },
          })
          return record
        else
          return old_resolve(name, ...)
        end
      end
      client.toip = function(name, ...)
        if name == "mashape.test" then
          return "1.2.3.4", ...
        else
          return old_toip(name, ...)
        end
      end

      -- insert 2nd address
      dnsA({
        { name = "getkong.test", address = "9.9.9.9", ttl = 60*60 },
      })

      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 50 },
          { name = "getkong.test", port = 123, weight = 50 },
        },
        dns = client,
        wheelSize = 100,
        ttl0 = 2,
      })

      local count = count_indices(b)
      assert.same({
        ["mashape.test:80"] = 1,
        ["9.9.9.9:123"] = 1,
      }, count)

      -- update weights
      add_target(b, "mashape.test", 80, 150)

      count = count_indices(b)
      assert.same({
        ["mashape.test:80"] = 3,
        ["9.9.9.9:123"] = 1,
      }, count)
    end)
    it("weight change for unresolved record, updates properly", function()
      local record = dnsA({
        { name = "really.really.really.does.not.exist.hostname.test", address = "1.2.3.4" },
      })
      dnsAAAA({
        { name = "getkong.test", address = "::1" },
      })
      local b = check_balancer(new_balancer {
        dns = client,
        wheelSize = 60,
        requery = 0.1,
      })
      add_target(b, "really.really.really.does.not.exist.hostname.test", 80, 10)
      add_target(b, "getkong.test", 80, 10)
      local count = count_indices(b)
      assert.same({
        ["1.2.3.4:80"] = 1,
        ["[::1]:80"]   = 1,
      }, count)

      -- expire the existing record
      record.expire = 0
      record.expired = true
      -- do a lookup to trigger the async lookup
      client.resolve("really.really.really.does.not.exist.hostname.test", {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_indices(b)
      assert.same({
        --["1.2.3.4:80"] = 0,  --> failed to resolve, no more entries
        ["[::1]:80"]   = 1,
      }, count)

      -- update the failed record
      add_target(b, "really.really.really.does.not.exist.hostname.test", 80, 20)
      -- reinsert a cache entry
      dnsA({
        { name = "really.really.really.does.not.exist.hostname.test", address = "1.2.3.4" },
      })
      sleep(2)  -- wait for timer to re-resolve the record
      targets.resolve_targets(b.targets)

      count = count_indices(b)
      assert.same({
        ["1.2.3.4:80"] = 2,
        ["[::1]:80"]   = 1,
      }, count)
    end)
    it("weight change SRV record, has no effect", function()
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsSRV({
        { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5 },
        { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5 },
      })
      local b = check_balancer(new_balancer {
        dns = client,
        wheelSize = 120,
      })
      add_target(b, "mashape.test", 80, 10)
      add_target(b, "gelato.test", 80, 10)  --> port + weight will be ignored
      local count = count_indices(b)
      local state = copyWheel(b)
      assert.same({
        ["1.2.3.4:80"]   = 2,
        ["1.2.3.5:80"]   = 2,
        ["1.2.3.6:8001"] = 1,
        ["1.2.3.6:8002"] = 1,
      }, count)

      add_target(b, "gelato.test", 80, 20)  --> port + weight will be ignored
      count = count_indices(b)
      assert.same({
        ["1.2.3.4:80"]   = 2,
        ["1.2.3.5:80"]   = 2,
        ["1.2.3.6:8001"] = 1,
        ["1.2.3.6:8002"] = 1,
      }, count)
      assert.same(state, copyWheel(b))
    end)
    it("renewed DNS A record; no changes", function()
      local record = dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 5 },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 100,
      })
      local state = copyWheel(b)
      record.expire = gettime() -1 -- expire current dns cache record
      dnsA({   -- create a new record (identical)
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      -- create a spy to check whether dns was queried
      spy.on(client, "resolve")
      for _ = 1, b.wheelSize do -- call all, to make sure we hit the expired one
        b:getPeer()  -- invoke balancer, to expire record and re-query dns
      end
      assert.spy(client.resolve).was_called_with("mashape.test",nil, nil)
      assert.same(state, copyWheel(b))
    end)

    it("renewed DNS AAAA record; no changes", function()
      local record = dnsAAAA({
        { name = "mashape.test", address = "::1" },
        { name = "mashape.test", address = "::2" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 5 },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 100,
      })
      local state = copyWheel(b)
      record.expire = gettime() -1 -- expire current dns cache record
      dnsAAAA({   -- create a new record (identical)
        { name = "mashape.test", address = "::1" },
        { name = "mashape.test", address = "::2" },
      })
      -- create a spy to check whether dns was queried
      spy.on(client, "resolve")
      for _ = 1, b.wheelSize do -- call all, to make sure we hit the expired one
        b:getPeer()  -- invoke balancer, to expire record and re-query dns
      end
      assert.spy(client.resolve).was_called_with("mashape.test",nil, nil)
      assert.same(state, copyWheel(b))
    end)
    it("renewed DNS SRV record; no changes", function()
      local record = dnsSRV({
        { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5 },
        { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5 },
        { name = "gelato.test", target = "1.2.3.6", port = 8003, weight = 5 },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "gelato.test" },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 100,
      })
      local state = copyWheel(b)
      record.expire = gettime() -1 -- expire current dns cache record
      dnsSRV({    -- create a new record (identical)
        { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5 },
        { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5 },
        { name = "gelato.test", target = "1.2.3.6", port = 8003, weight = 5 },
      })
      -- create a spy to check whether dns was queried
      spy.on(client, "resolve")
      for _ = 1, b.wheelSize do -- call all, to make sure we hit the expired one
        b:getPeer()  -- invoke balancer, to expire record and re-query dns
      end
      assert.spy(client.resolve).was_called_with("gelato.test",nil, nil)
      assert.same(state, copyWheel(b))
    end)
    it("renewed DNS A record; address changes", function()
      local record = dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
        { name = "getkong.test", address = "8.8.8.8" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 10 },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 100,
      })
      local state = copyWheel(b)
      record.expire = gettime() -1 -- expire current dns cache record
      dnsA({                       -- insert an updated record
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.6" },  -- target updated
      })
      -- run entire wheel to make sure the expired one is requested, and updated
      for _ = 1, b.wheelSize do b:getPeer() end
      -- all old 'mashape.test @ 1.2.3.5' should now be 'mashape.test @ 1.2.3.6'
      -- and more important; all others should not have moved indices/positions!
      updateWheelState(state, " %- 1%.2%.3%.5 @ ", " - 1.2.3.6 @ ")
      -- FIXME: this test depends on wheel sorting, which is not good
      --assert.same(state, copyWheel(b))
    end)
    it("renewed DNS A record; failed #slow", function()
      -- This test might show some error output similar to the lines below. This is expected and ok.
      -- 2016/11/07 16:48:33 [error] 81932#0: *2 recv() failed (61: Connection refused), context: ngx.timer

      local record = dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 10 },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 20,
        requery = 0.1,   -- shorten default requery time for the test
      })
      copyWheel(b)
      copyWheel(b)
      -- reconfigure the dns client to make sure next query fails
      assert(client.init {
        hosts = {},
        resolvConf = {
          "nameserver 127.0.0.1:22000" -- make sure dns query fails
        },
      })
      record.expire = gettime() -1 -- expire current dns cache record
      -- run entire wheel to make sure the expired one is requested, so it can fail
      for _ = 1, b.wheelSize do b:getPeer() end
      -- the only indice is now getkong.test
      assert.same({"1 - 9.9.9.9 @ 123 (getkong.test)" }, copyWheel(b))

      -- reconfigure the dns client to make sure next query works again
      assert(client.init {
        hosts = {},
        resolvConf = {
          "nameserver 8.8.8.8"
        },
      })
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      sleep(b.requeryInterval + 2) --requery timer runs, so should be fixed after this

      -- wheel should be back in original state
      -- FIXME: this test depends on wheel sorting, which is not good
      --assert.same(state1, copyWheel(b))
    end)
    it("renewed DNS A record; last host fails DNS resolution #slow", function()
      -- This test might show some error output similar to the lines below. This is expected and ok.
      -- 2017/11/06 15:52:49 [warn] 5123#0: *2 [lua] balancer.lua:320: queryDns(): [ringbalancer] querying dns for really.really.really.does.not.exist.hostname.test failed: dns server error: 3 name error, context: ngx.timer

      local test_name = "really.really.really.does.not.exist.hostname.test"
      local ttl = 0.1
      local staleTtl = 0   -- stale ttl = 0, force lookup upon expiring
      local record = dnsA({
        { name = test_name, address = "1.2.3.4", ttl = ttl },
      }, staleTtl)
      local b = check_balancer(new_balancer {
        hosts = {
          { name = test_name, port = 80, weight = 10 },
        },
        dns = client,
      })
      for _ = 1, b.wheelSize do
        local ip = b:getPeer()
        assert.equal(record[1].address, ip)
      end
      -- wait for ttl to expire
      sleep(ttl + 0.1)
      targets.resolve_targets(b.targets)
      -- run entire wheel to make sure the expired one is requested, so it can fail
      for _ = 1, b.wheelSize do
        local ip, port = b:getPeer()
        assert.is_nil(ip)
        assert.equal(port, "Balancer is unhealthy")
      end
    end)
    it("renewed DNS A record; unhealthy entries remain unhealthy after renewal", function()
      local record = dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 5 },
          { name = "getkong.test", port = 123, weight = 10 },
        },
        dns = client,
        wheelSize = 20,
      })

      -- mark node down
      assert(b:setAddressStatus(b:findAddress("1.2.3.4", 80, "mashape.test"), false))

      -- run the wheel
      local res = {}
      for _ = 1, 15 do
        local addr, port, host = b:getPeer()
        res[addr..":"..port] = (res[addr..":"..port] or 0) + 1
        res[host..":"..port] = (res[host..":"..port] or 0) + 1
      end

      assert.equal(nil, res["1.2.3.4:80"])    -- unhealthy node gets no hits, key never gets initialized
      assert.equal(5, res["1.2.3.5:80"])
      assert.equal(5, res["mashape.test:80"])
      assert.equal(10, res["9.9.9.9:123"])
      assert.equal(10, res["getkong.test:123"])

      local state = copyWheel(b)

      record.expire = gettime() -1 -- expire current dns cache record
      dnsA({   -- create a new record (identical)
        { name = "mashape.test", address = "1.2.3.4" },
        { name = "mashape.test", address = "1.2.3.5" },
      })
      -- create a spy to check whether dns was queried
      spy.on(client, "resolve")
      for _ = 1, b.wheelSize do -- call all, to make sure we hit the expired one
        b:getPeer()  -- invoke balancer, to expire record and re-query dns
      end
      assert.spy(client.resolve).was_called_with("mashape.test",nil, nil)
      assert.same(state, copyWheel(b))

      -- run the wheel again
      local res2 = {}
      for _ = 1, 15 do
        local addr, port, host = b:getPeer()
        res2[addr..":"..port] = (res2[addr..":"..port] or 0) + 1
        res2[host..":"..port] = (res2[host..":"..port] or 0) + 1
      end

      -- results are identical: unhealthy node remains unhealthy
      assert.same(res, res2)

    end)
    it("low weight with zero-indices assigned doesn't fail", function()
      -- depending on order of insertion it is either 1 or 0 indices
      -- but it may never error.
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 99999 },
          { name = "getkong.test", port = 123, weight = 1 },
        },
        dns = client,
        wheelSize = 100,
      })
      -- Now the order reversed (weights exchanged)
      dnsA({
        { name = "mashape.test", address = "1.2.3.4" },
      })
      dnsA({
        { name = "getkong.test", address = "9.9.9.9" },
      })
      check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 1 },
          { name = "getkong.test", port = 123, weight = 99999 },
        },
        dns = client,
        wheelSize = 100,
      })
    end)
    it("SRV record with 0 weight doesn't fail resolving", function()
      -- depending on order of insertion it is either 1 or 0 indices
      -- but it may never error.
      dnsSRV({
        { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 0 },
        { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 0 },
      })
      local b = check_balancer(new_balancer {
        hosts = {
          -- port and weight will be overridden by the above
          { name = "gelato.test", port = 80, weight = 99999 },
        },
        dns = client,
        wheelSize = 100,
      })
      local ip, port = b:getPeer()
      assert.equal("1.2.3.6", ip)
      assert(port == 8001 or port == 8002, "port expected 8001 or 8002")
    end)
    it("ttl of 0 inserts only a single unresolved address", function()
      local ttl = 0
      local resolve_count = 0
      local toip_count = 0

      -- mock the resolve/toip methods
      local old_resolve = client.resolve
      local old_toip = client.toip
      finally(function()
        client.resolve = old_resolve
        client.toip = old_toip
      end)
      client.resolve = function(name, ...)
        if name == "mashape.test" then
          local record = dnsA({
            { name = "mashape.test", address = "1.2.3.4", ttl = ttl },
          })
          resolve_count = resolve_count + 1
          return record
        else
          return old_resolve(name, ...)
        end
      end
      client.toip = function(name, ...)
        if name == "mashape.test" then
          toip_count = toip_count + 1
          return "1.2.3.4", ...
        else
          return old_toip(name, ...)
        end
      end

      -- insert 2nd address
      dnsA({
        { name = "getkong.test", address = "9.9.9.9", ttl = 60*60 },
      })

      local b = check_balancer(new_balancer {
        hosts = {
          { name = "mashape.test", port = 80, weight = 50 },
          { name = "getkong.test", port = 123, weight = 50 },
        },
        dns = client,
        wheelSize = 100,
        ttl0 = 2,
      })
      -- get current state
      local state = copyWheel(b)
      -- run it down, count the dns queries done
      for _ = 1, b.wheelSize do b:getPeer() end
      assert.equal(b.wheelSize/2, toip_count)  -- one resolver hit for each index
      assert.equal(1, resolve_count) -- hit once, when adding the host to the balancer

      ttl = 60 -- set our records ttl to 60 now, so we only get one extra hit now
      toip_count = 0  --reset counters
      resolve_count = 0
      -- wait for expiring the 0-ttl setting
      sleep(b.ttl0Interval + 1)  -- 0 ttl will be requeried, to check for changed ttl

      -- run it down, count the dns queries done
      for _ = 1, b.wheelSize do b:getPeer() end
      --assert.equal(0, toip_count)   -- TODO:  must it be 0?
      assert.equal(1, resolve_count) -- hit once, when updating the 0-ttl entry

      -- finally check whether indices didn't move around
      updateWheelState(state, " %- mashape%.test @ ", " - 1.2.3.4 @ ")
      copyWheel(b)
      -- FIXME: this test depends on wheel sorting, which is not good
      --assert.same(state, copyWheel(b))
    end)
    it("recreate Kong issue #2131", function()
      -- erasing does not remove the address from the host
      -- so if the same address is added again, and then deleted again
      -- then upon erasing it will find the previous erased address object,
      -- and upon erasing again a nil-referencing issue then occurs
      local ttl = 1
      local record
      local hostname = "dnstest.mashape.test"

      -- mock the resolve/toip methods
      local old_resolve = client.resolve
      local old_toip = client.toip
      finally(function()
        client.resolve = old_resolve
        client.toip = old_toip
      end)
      client.resolve = function(name, ...)
        if name == hostname then
          record = dnsA({
            { name = hostname, address = "1.2.3.4", ttl = ttl },
          })
          return record
        else
          return old_resolve(name, ...)
        end
      end
      client.toip = function(name, ...)
        if name == hostname then
          return "1.2.3.4", ...
        else
          return old_toip(name, ...)
        end
      end

      -- create a new balancer
      local b = check_balancer(new_balancer {
        hosts = {
          { name = hostname, port = 80, weight = 50 },
        },
        dns = client,
        wheelSize = 10,
        ttl0 = 1,
      })

      sleep(1.1) -- wait for ttl to expire
      -- fetch a peer to reinvoke dns and update balancer, with a ttl=0
      ttl = 0
      b:getPeer()   --> force update internal from A to SRV
      sleep(1.1) -- wait for ttl0, as provided to balancer, to expire
      -- restore ttl to non-0, and fetch a peer to update balancer
      ttl = 1
      b:getPeer()   --> force update internal from SRV to A
      sleep(1.1) -- wait for ttl to expire
      -- fetch a peer to reinvoke dns and update balancer, with a ttl=0
      ttl = 0
      b:getPeer()   --> force update internal from A to SRV
    end)
  end)
end)
