Skip to content

Commit

Permalink
Merge pull request #68 from neighbourhoodie/fix/improve-assert
Browse files Browse the repository at this point in the history
fix: improve failure logging
  • Loading branch information
garbados committed Jun 27, 2020
2 parents 55f3f2e + b13eeb5 commit e17ed8c
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 93 deletions.
102 changes: 52 additions & 50 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,6 @@ async function getConsent (question) {
})
}

function catchError (error) {
console.log('ERROR')
if (error.error === 'not_found') {
console.log('Primary database does not exist. There is nothing to migrate.')
} else if (error.error === 'unauthorized') {
console.log('Could not authenticate with CouchDB. Are the credentials correct?')
} else if (error.code === 'EACCES') {
console.log('Could not access the checkpoint document. Are you running as a different user?')
} else {
console.log('Unexpected error: %j', error)
}
process.exit(1)
}

function generalOptions (yargs) {
return yargs
// backwards compat with old flag names
Expand Down Expand Up @@ -139,13 +125,17 @@ require('yargs')
handler: async function (argv) {
const continuum = getContinuum(argv)
log(`Migrating database: ${continuum.source.host}${continuum.source.pathname}`)
try {
await continuum.createReplica()
const consent = await getConsent()
if (!consent) return log('Could not acquire consent. Exiting...')
await continuum.replacePrimary()
console.log(`Migrated database: ${continuum.source.host}${continuum.source.pathname}`)
} catch (error) { catchError(error) }
await continuum.createReplica()
const consent1 = await getConsent()
if (!consent1) return log('Could not acquire consent. Exiting...')
await continuum.replacePrimary()
console.log(`Migrated database: ${continuum.source.host}${continuum.source.pathname}`)
log(`Migrating database: ${continuum.source.host}${continuum.source.pathname}`)
await continuum.createReplica()
const consent2 = await getConsent()
if (!consent2) return log('Could not acquire consent. Exiting...')
await continuum.replacePrimary()
console.log(`Migrated database: ${continuum.source.host}${continuum.source.pathname}`)
}
})
.command({
Expand All @@ -156,10 +146,11 @@ require('yargs')
handler: async function (argv) {
const continuum = getContinuum(argv)
log(`Creating replica of ${continuum.source.host}${continuum.source.pathname} at ${continuum.target.host}${continuum.target.pathname}`)
try {
await continuum.createReplica()
console.log(`Created replica of ${continuum.source.host}${continuum.source.path}`)
} catch (error) { catchError(error) }
await continuum.createReplica()
console.log(`Created replica of ${continuum.source.host}${continuum.source.pathname}`)
log(`Creating replica of ${continuum.source.host}${continuum.source.pathname} at ${continuum.target.host}${continuum.target.pathname}`)
await continuum.createReplica()
console.log(`Created replica of ${continuum.source.host}${continuum.source.pathname}`)
}
})
.command({
Expand All @@ -169,13 +160,11 @@ require('yargs')
builder: generalOptions,
handler: async function (argv) {
const continuum = getContinuum(argv)
log(`Replacing primary ${continuum.source.host}${continuum.source.path} with ${continuum.target.host}${continuum.target.path}`)
try {
const consent = await getConsent()
if (!consent) return log('Could not acquire consent. Exiting...')
await continuum.replacePrimary()
console.log(`Successfully replaced ${continuum.source.host}${continuum.source.path}`)
} catch (error) { catchError(error) }
log(`Replacing primary ${continuum.source.host}${continuum.source.pathname} with ${continuum.target.host}${continuum.target.pathname}`)
const consent = await getConsent()
if (!consent) return log('Could not acquire consent. Exiting...')
await continuum.replacePrimary()
console.log(`Successfully replaced ${continuum.source.host}${continuum.source.pathname}`)
}
})
.command({
Expand Down Expand Up @@ -227,26 +216,39 @@ require('yargs')
handler: async function (argv) {
const { couchUrl, verbose } = argv
if (verbose) { process.env.LOG = true }
try {
const dbNames = await CouchContinuum.getRemaining(couchUrl)
if (!dbNames.length) {
console.log('No eligible databases to migrate.')
return
}
const continuums = dbNames.map((dbName) => {
return new CouchContinuum({ dbName, ...argv })
})
log('Creating replicas...')
await CouchContinuum.createReplicas(continuums)
const consent = await getConsent('Ready to replace primaries with replicas. Continue? [y/N] ')
if (!consent) return console.log('Could not acquire consent. Exiting...')
log('Replacing primaries...')
await CouchContinuum.replacePrimaries(continuums)
await CouchContinuum.removeCheckpoint()
console.log(`Successfully migrated databases: ${dbNames.join(', ')}`)
} catch (error) { catchError(error) }
const dbNames = await CouchContinuum.getRemaining(couchUrl)
if (!dbNames.length) {
console.log('No eligible databases to migrate.')
return
}
const continuums = dbNames.map((source) => {
return new CouchContinuum({ source, ...argv })
})
log('Creating replicas...')
await CouchContinuum.createReplicas(continuums)
const consent = await getConsent('Ready to replace primaries with replicas. Continue? [y/N] ')
if (!consent) return console.log('Could not acquire consent. Exiting...')
log('Replacing primaries...')
await CouchContinuum.replacePrimaries(continuums)
await CouchContinuum.removeCheckpoint()
console.log(`Successfully migrated databases: ${dbNames.join(', ')}`)
}
})
.config()
.alias('h', 'help')
.fail((msg, error, yargs) => {
if (!error) {
console.log(msg)
} else if (error.error === 'not_found') {
console.log('Primary database does not exist. There is nothing to migrate.')
} else if (error.error === 'unauthorized') {
console.log('Could not authenticate with CouchDB. Are the credentials correct?')
} else if (error.code === 'EACCES') {
console.log('Could not access the checkpoint document. Are you running as a different user?')
} else {
console.log('Unexpected error. Please report this so we can fix it!')
console.log(error)
}
process.exit(1)
})
.parse()
120 changes: 87 additions & 33 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const assert = require('assert')
const assert = require('assert').strict
const path = require('path')
const ProgressBar = require('progress')
const { URL, format: urlFormat } = require('url')
Expand Down Expand Up @@ -56,7 +56,14 @@ class CouchContinuum {
}

static async removeCheckpoint () {
await unlink(checkpoint)
try {
await unlink(checkpoint)
} catch (error) {
// don't complain if checkpoint is already missing
if (error.code !== 'ENOENT') {
throw error
}
}
}

static async getRemaining (couchUrl) {
Expand All @@ -82,25 +89,51 @@ class CouchContinuum {
}

static async _isAvailable (dbUrl) {
const { down } = await request({
url: `${dbUrl}/_local/in-maintenance`,
json: true
})
return !down
try {
const { down } = await request({
url: `${dbUrl}/_local/in-maintenance`,
json: true
})
return !down
} catch (error) {
if (error.error !== 'not_found') {
throw error
} else {
// document doesn't exist, so, db must be available
return true
}
}
}

static async _setUnavailable (dbUrl) {
await request({
url: `${dbUrl}/_local/in-maintenance`,
method: 'PUT',
json: { down: true }
})
const url = `${dbUrl}/_local/in-maintenance`
try {
// update
const { _rev: rev } = await request({ url, json: true })
return request({ url, json: { _rev: rev, down: true }, method: 'PUT' })
} catch (error) {
if (error.error === 'not_found') {
// create
await request({ url, method: 'PUT', json: { down: true } })
} else {
throw error
}
}
}

static async _setAvailable (dbUrl) {
const url = `${dbUrl}/_local/in-maintenance`
const { _rev: rev } = await request({ url, json: true })
return request({ url, qs: { rev }, method: 'DELETE' })
try {
const { _rev: rev } = await request({ url, json: true })
return request({ url, qs: { rev }, method: 'DELETE' })
} catch (error) {
if (error.error === 'not_found') {
// already available, nothing to do
return null
} else {
throw error
}
}
}

constructor ({
Expand Down Expand Up @@ -166,12 +199,19 @@ class CouchContinuum {
if (this.q) { qs.q = this.q }
if (this.n) { qs.n = this.n }
if (this.placement) { qs.placement = this.placement }
return request({
url: dbUrl,
method: 'PUT',
qs,
json: true
})
try {
const result = await request({
url: dbUrl,
method: 'PUT',
qs,
json: true
})
return result
} catch (error) {
if (error.error !== 'file_exists') {
throw error
}
}
}

async _destroyDb (dbUrl) {
Expand Down Expand Up @@ -257,18 +297,32 @@ class CouchContinuum {
*/
async _isInUse (dbName) {
// TODO check all known hosts
const activeTasks = await request({
url: `${this.url.href}_active_tasks`,
json: true
})
const { jobs } = await request({
url: `${this.url.href}_scheduler/jobs`,
json: true
}).then(({ jobs }) => {
return { jobs: jobs || [] }
})
for (const { database } of [...jobs, ...activeTasks]) {
assert.notStrictEqual(database, dbName, `${dbName} is still in use.`)
try {
const activeTasks = await request({
url: `${this.url.href}_active_tasks`,
json: true
})
const { jobs } = await request({
url: `${this.url.href}_scheduler/jobs`,
json: true
}).then(({ jobs }) => {
return { jobs: jobs || [] }
})
for (const { database, source, target } of [...jobs, ...activeTasks]) {
const re = new RegExp(`/${dbName}/`)
if (database) {
assert.strictEqual(re.test(database), true)
}
assert.strictEqual(re.test(source), true)
assert.strictEqual(re.test(target), true)
}
} catch (error) {
if (error.error === 'illegal_database_name') {
// 1.x -- this block is just for travis' test conditions
return null
} else {
throw error
}
}
}

Expand Down Expand Up @@ -298,7 +352,7 @@ class CouchContinuum {
}

async replacePrimary () {
log(`Replacing primary ${this.source.host}${this.source.pathname} using ${this.target.host}${this.target.path}...`)
log(`Replacing primary ${this.source.host}${this.source.pathname} using ${this.target.host}${this.target.pathname}...`)
log('[0/8] Checking if primary is in use...')
await this._isInUse(this.source.pathname.slice(1))
log('[1/8] Verifying primary and replica match...')
Expand Down
18 changes: 17 additions & 1 deletion lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@ module.exports = async function (options) {
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) return reject(err)
return resolve(body)
if (res.statusCode >= 400) {
let string, json
if (typeof body === 'string') {
string = body
json = { options, ...JSON.parse(body) }
} else {
string = JSON.stringify(body)
json = { options, ...body }
}
const error = new Error(string)
Object.entries(json).map(([prop, value]) => {
error[prop] = value
})
return reject(error)
} else {
return resolve(body)
}
})
})
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"couch-continuum": "bin.js"
},
"scripts": {
"test": "standard && dependency-check . --unused --no-dev && mocha && npm audit"
"test": "standard && dependency-check . --unused --no-dev && LOG=true mocha && npm audit"
},
"author": "Diana Thayer <garbados@gmail.com>",
"license": "Apache-2.0",
Expand Down
Loading

0 comments on commit e17ed8c

Please sign in to comment.