We used to update tombstoned column when a DNS record is deleted. Records with same name, type and data can be added and removed many times. We also tried to keep only one copy of active record using a unique constraint. This provides nice size reduction, but it complicates record processing. Especially when a deleted record is added back, we don't have a way to know which request came last (thus whether should we create or remove the record in the DNS server). We fix this situation by adding separate DNS record entry for each insert and delete request. We also tag each record with a timestamp. Now we can process the records in the request order. Adding separate records for each operation increases the total size. To remove unnecessary bloat, we expand the purge logic to also purge obsoleted records.
119 lines
5.1 KiB
Ruby
119 lines
5.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../../model/spec_helper"
|
|
|
|
RSpec.describe Prog::DnsZone::DnsZoneNexus do
|
|
subject(:nx) { described_class.new(Strand.new(id: DnsZone.generate_uuid)) }
|
|
|
|
let(:dns_zone) { DnsZone.create_with_id(project_id: SecureRandom.uuid, name: "postgres.ubicloud.com") }
|
|
let(:dns_server) { DnsServer.create_with_id(name: "ns.ubicloud.com") }
|
|
let(:vm) { instance_double(Vm, id: "788525ed-d6f0-4937-a844-323d4fd91946") }
|
|
let(:sshable) { instance_double(Sshable) }
|
|
|
|
before do
|
|
allow(vm).to receive(:sshable).and_return(sshable)
|
|
allow(dns_server).to receive(:vms).and_return([vm])
|
|
allow(dns_zone).to receive(:dns_servers).and_return([dns_server])
|
|
allow(nx).to receive(:dns_zone).and_return(dns_zone)
|
|
end
|
|
|
|
describe "#wait" do
|
|
it "hops to refresh_dns_servers if refresh_dns_servers semaphore is set" do
|
|
expect(nx).to receive(:when_refresh_dns_servers_set?).and_yield
|
|
expect { nx.wait }.to hop("refresh_dns_servers")
|
|
end
|
|
|
|
it "hops to purge_dns_records if if last purge happened more than 1 hour ago" do
|
|
expect(dns_zone).to receive(:last_purged_at).and_return(Time.now - 60 * 60 * 2)
|
|
expect { nx.wait }.to hop("purge_dns_records")
|
|
end
|
|
|
|
it "naps if there is nothing to do" do
|
|
expect { nx.wait }.to nap(10)
|
|
end
|
|
end
|
|
|
|
describe "#refresh_dns_servers" do
|
|
before do
|
|
r1 = DnsRecord.create_with_id(name: "test-pg-1", type: "A", ttl: 10, data: "1.2.3.4")
|
|
r2 = DnsRecord.create_with_id(name: "test-pg-2", type: "A", ttl: 10, data: "5.6.7.8")
|
|
r3 = DnsRecord.create_with_id(name: "test-pg-3", type: "A", ttl: 10, data: "9.10.11.12", tombstoned: true)
|
|
|
|
dns_zone.add_record(r1)
|
|
dns_zone.add_record(r2)
|
|
dns_zone.add_record(r3)
|
|
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{r1.id}', '#{dns_server.id}')"].insert
|
|
end
|
|
|
|
it "does not push anything if there is no unseen records" do
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers SELECT id, '#{dns_server.id}' FROM dns_record"].insert
|
|
|
|
expect(sshable).not_to receive(:cmd)
|
|
expect { nx.refresh_dns_servers }.to hop("wait")
|
|
end
|
|
|
|
it "gathers unseen records for each dns server and pushes them to dns servers" do
|
|
expected_commands = <<COMMANDS
|
|
zone-abort postgres.ubicloud.com
|
|
zone-begin postgres.ubicloud.com
|
|
zone-set postgres.ubicloud.com test-pg-2 10 A 5.6.7.8
|
|
zone-unset postgres.ubicloud.com test-pg-3 10 A 9.10.11.12
|
|
zone-commit postgres.ubicloud.com
|
|
COMMANDS
|
|
|
|
expect(sshable).to receive(:cmd).with("sudo -u knot knotc", stdin: expected_commands.chomp).and_return("OK\nOK\nOK\nOK\nOK")
|
|
expect { nx.refresh_dns_servers }.to hop("wait")
|
|
end
|
|
|
|
it "ignores unimportant errors" do
|
|
expect(sshable).to receive(:cmd).and_return("no active transaction\nOK\nsuch record already exists in zone\nno such record in zone found\nOK")
|
|
expect { nx.refresh_dns_servers }.to hop("wait")
|
|
end
|
|
|
|
it "raises an exception for unexpected failures" do
|
|
expect(sshable).to receive(:cmd).and_return("error in zone-abort\nOK\nOK\nOK\nOK")
|
|
|
|
expect {
|
|
nx.refresh_dns_servers
|
|
}.to raise_error RuntimeError, "Rectify failed on #{dns_server}. Command: zone-abort postgres.ubicloud.com. Output: error in zone-abort"
|
|
end
|
|
end
|
|
|
|
describe "#purge_dns_records" do
|
|
it "deletes obsoleted records, seen or unseen" do
|
|
r1 = DnsRecord.create_with_id(created_at: Time.now - 1, name: "test-pg-1", type: "A", ttl: 10, data: "1.2.3.4")
|
|
r2 = DnsRecord.create_with_id(created_at: Time.now, name: "test-pg-1", type: "A", ttl: 10, data: "1.2.3.4")
|
|
r3 = DnsRecord.create_with_id(created_at: Time.now + 1, name: "test-pg-1", type: "A", ttl: 10, data: "1.2.3.4")
|
|
|
|
dns_zone.add_record(r1)
|
|
dns_zone.add_record(r2)
|
|
dns_zone.add_record(r3)
|
|
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{r1.id}', '#{dns_server.id}')"].insert
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{r3.id}', '#{dns_server.id}')"].insert
|
|
|
|
expect { nx.purge_dns_records }.to hop("wait")
|
|
expect(dns_zone.reload.records.count).to eq(1)
|
|
expect(DB[:seen_dns_records_by_dns_servers].all.count).to eq(1)
|
|
end
|
|
|
|
it "deletes seen tombstoned records" do
|
|
r1 = DnsRecord.create_with_id(name: "test-pg-1", type: "A", ttl: 10, data: "1.2.3.4")
|
|
r2 = DnsRecord.create_with_id(name: "test-pg-2", type: "A", ttl: 10, data: "5.6.7.8", tombstoned: true)
|
|
r3 = DnsRecord.create_with_id(name: "test-pg-3", type: "A", ttl: 10, data: "9.10.11.12", tombstoned: true)
|
|
|
|
dns_zone.add_record(r1)
|
|
dns_zone.add_record(r2)
|
|
dns_zone.add_record(r3)
|
|
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{r1.id}', '#{dns_server.id}')"].insert
|
|
DB["INSERT INTO seen_dns_records_by_dns_servers(dns_record_id, dns_server_id) VALUES('#{r2.id}', '#{dns_server.id}')"].insert
|
|
|
|
expect { nx.purge_dns_records }.to hop("wait")
|
|
expect(dns_zone.reload.records.count).to eq(2)
|
|
expect(DB[:seen_dns_records_by_dns_servers].all.count).to eq(1)
|
|
end
|
|
end
|
|
end
|