Skip to content

Commit

Permalink
feat(speed): make adding records much faster (#15)
Browse files Browse the repository at this point in the history
- for a zone with 30,000 records, reduce insertion time from 7 minutes to ~3 seconds.
- method: generate an owner index, use for duplicate and collision detection (vs linear array search)
- add node 18 testing
  • Loading branch information
msimerson authored May 9, 2022
1 parent ba91800 commit dc6560b
Show file tree
Hide file tree
Showing 6 changed files with 5,045 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
node-version: [ 14, 16 ]
node-version: [ 14, 16, 18 ]
fail-fast: false

steps:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
#### 1.N.N - YYYY-MM-DD


#### 1.1.0 - 2022-05-09

- feat(speed): make adding records much faster
- for a zone with 30,000 records, reduce insertion time from 7 minutes to ~3 seconds.
- method: populate owner index, use for duplicate and collision detection (vs linear array search)
- test: add windows CI testing
- test(bind): replace implicit \n with os.EOL
- test(index): add tests for removeChar, stripComment
Expand Down
42 changes: 30 additions & 12 deletions lib/zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default class ZONE extends Map {

this.RR = []
this.SOA = {}
this.ownerIdx = {}

if (opts.origin) this.setOrigin(opts.origin)
this.setTTL(opts.ttl)
Expand Down Expand Up @@ -56,7 +57,7 @@ export default class ZONE extends Map {
default:
}

this.RR.push(rr)
this.append(rr)
}

addCname (rr) {
Expand All @@ -76,13 +77,23 @@ export default class ZONE extends Map {
}).length
if (conflicts) throw new Error(`owner already exists, CNAME not allowed, RFC 1034, 2181, & 4035`)

this.append(rr)
}

append (rr) {
// optimization: create owner index, so searches happen in O(1) vs O(n) time,
// matters when zone has 1000+ records
const owner = rr.get('owner')
if (this.ownerIdx[owner] === undefined) this.ownerIdx[owner] = []
this.ownerIdx[owner].push(this.RR.length)

this.RR.push(rr)
}

getRR (rr) {
const fields = rr.getFields()

return this.RR.filter(r => {
return this.getOwnerMatches(rr).filter(r => {

const fieldDiffs = fields.map(f => {
return r.get(f) === rr.get(f)
Expand All @@ -97,11 +108,11 @@ export default class ZONE extends Map {
const ownerMatches = this.getOwnerMatches(rr)
if (ownerMatches.length === 0) return

const allowedTypes = 'CNAME SIG NXT KEY NSEC RRSIG'.split(' ')

// CNAME conflicts with almost everything, assure no CNAME at this name
if (!'CNAME SIG NXT KEY NSEC RRSIG'.split(' ').includes(rr.get('type'))) {
const conflicts = ownerMatches.filter(r => {
return r.get('type') === 'CNAME'
}).length
if (!allowedTypes.includes(rr.get('type'))) {
const conflicts = ownerMatches.filter(r => r.get('type') === 'CNAME').length
if (conflicts) throw new Error(`owner exists as CNAME, not allowed, RFC 1034, 2181, & 4035`)
}
}
Expand All @@ -112,10 +123,10 @@ export default class ZONE extends Map {
}

itMatchesSetTTL (rr) {
// a Resource Record Set exists...with the same label, class, type (different data)
const matches = this.RR.filter(r => {
// a RR Set exists...with the same label(owner), class, type (different data)
const matches = this.getOwnerMatches(rr).filter(r => {

const diffs = [ 'owner', 'class', 'type' ].map(f => {
const diffs = [ 'class', 'type' ].map(f => {
return r.get(f) === rr.get(f)
}).filter(m => m === false).length

Expand All @@ -128,9 +139,16 @@ export default class ZONE extends Map {

getOwnerMatches (rr) {
const owner = rr.get('owner')
return this.RR.filter(r => {
return r.get('owner') === owner
})

if (this.ownerIdx[owner] === undefined) return []

// optimized, retrieves matching owners via index
return this.ownerIdx[owner].map(n => this.RR[n])

// not optimized, searches length of array for matches
// return this.RR.filter(r => {
// return r.get('owner') === owner
// })
}

setOrigin (val) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dns-zone-validator",
"version": "1.0.0",
"version": "1.1.0",
"description": "DNS Zone",
"main": "index.js",
"type": "module",
Expand Down
13 changes: 13 additions & 0 deletions test/dns-zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,17 @@ bounce.theartfarm.com.\t+86400\tCNAME\tcustom-email-domain.stripe.com. ~
assert.ifError(e)
}
})

it('parses 5,000 entry zone file quickly', async function () {
const binPath = path.resolve('bin', 'dns-zone.js')
const args = [ binPath, '-i', 'bind', '-f', './test/fixtures/bind/example2.com', '-o', 'example2.com' ]
try {
const { stdout, stderr } = await execFile('node', args)
assert.strictEqual(stdout.split('\n').length, 4996)
assert.strictEqual(stderr, '')
}
catch (e) {
assert.ifError(e)
}
})
})
Loading

0 comments on commit dc6560b

Please sign in to comment.