couch-continuum
Advanced tools
Comparing version 2.0.1-alpha to 2.1.0-alpha
191
bin.js
#!/usr/bin/env node | ||
'use strict' | ||
const CouchContinuum = require('.') | ||
const readline = require('readline') | ||
const CouchContinuum = require('.') | ||
const log = require('./lib/log') | ||
/* | ||
@@ -11,16 +13,29 @@ HELPERS | ||
const prefix = '[couch-continuum]' | ||
function log () { | ||
arguments[0] = [prefix, arguments[0]].join(' ') | ||
console.log.apply(console, arguments) | ||
} | ||
function getContinuum (argv) { | ||
const { copyName, couchUrl, dbName, filterTombstones, interval, n, placement, q, verbose } = argv | ||
function getContinuum ({ | ||
couchUrl, | ||
filterTombstones, | ||
interval, | ||
n, | ||
placement, | ||
q, | ||
replicateSecurity, | ||
source, | ||
target, | ||
verbose | ||
}) { | ||
if (verbose) process.env.LOG = true | ||
const options = { copyName, couchUrl, dbName, filterTombstones, interval, n, placement, q } | ||
return new CouchContinuum(options) | ||
return new CouchContinuum({ | ||
couchUrl, | ||
filterTombstones, | ||
interval, | ||
n, | ||
placement, | ||
q, | ||
replicateSecurity, | ||
source, | ||
target | ||
}) | ||
} | ||
function getConsent (question) { | ||
async function getConsent (question) { | ||
question = question || 'Ready to replace the primary with the replica. Continue? [y/N] ' | ||
@@ -41,11 +56,11 @@ const rl = readline.createInterface({ | ||
function catchError (error) { | ||
log('ERROR') | ||
console.log('ERROR') | ||
if (error.error === 'not_found') { | ||
log('Primary database does not exist. There is nothing to migrate.') | ||
console.log('Primary database does not exist. There is nothing to migrate.') | ||
} else if (error.error === 'unauthorized') { | ||
log('Could not authenticate with CouchDB. Are the credentials correct?') | ||
console.log('Could not authenticate with CouchDB. Are the credentials correct?') | ||
} else if (error.code === 'EACCES') { | ||
log('Could not access the checkpoint document. Are you running as a different user?') | ||
console.log('Could not access the checkpoint document. Are you running as a different user?') | ||
} else { | ||
log('Unexpected error: %j', error) | ||
console.log('Unexpected error: %j', error) | ||
} | ||
@@ -64,28 +79,12 @@ process.exit(1) | ||
description: 'Migrate a database to new settings.', | ||
builder: function (yargs) { | ||
yargs.options({ | ||
dbName: { | ||
alias: 'N', | ||
description: 'The name of the database to modify.', | ||
required: true, | ||
type: 'string' | ||
}, | ||
copyName: { | ||
alias: 'c', | ||
description: 'The name of the database to use as a replica. Defaults to {dbName}_temp_copy', | ||
type: 'string' | ||
} | ||
}) | ||
}, | ||
handler: function (argv) { | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Migrating database '${argv.dbName}'...`) | ||
continuum.createReplica().then(function () { | ||
return getConsent() | ||
}).then((consent) => { | ||
log(`Migrating database: ${continuum.source.host}${continuum.source.path}`) | ||
try { | ||
await continuum.createReplica() | ||
const consent = await getConsent() | ||
if (!consent) return log('Could not acquire consent. Exiting...') | ||
return continuum.replacePrimary().then(() => { | ||
log('... success!') | ||
}) | ||
}).catch(catchError) | ||
await continuum.replacePrimary() | ||
console.log(`Migrated database: ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
} | ||
@@ -97,23 +96,9 @@ }) | ||
description: 'Create a replica of the given primary.', | ||
builder: function (yargs) { | ||
yargs.options({ | ||
dbName: { | ||
alias: 'n', | ||
description: 'The name of the database to modify.', | ||
required: true, | ||
type: 'string' | ||
}, | ||
copyName: { | ||
alias: 'c', | ||
description: 'The name of the database to use as a replica. Defaults to {dbName}_temp_copy', | ||
type: 'string' | ||
} | ||
}) | ||
}, | ||
handler: function (argv) { | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Creating replica of ${continuum.db1} at ${continuum.db2}`) | ||
continuum.createReplica().then(() => { | ||
log('... success!') | ||
}).catch(catchError) | ||
log(`Creating replica of ${continuum.source.host}${continuum.source.path} at ${continuum.target.host}${continuum.target.path}`) | ||
try { | ||
await continuum.createReplica() | ||
console.log(`Created replica of ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
} | ||
@@ -125,11 +110,11 @@ }) | ||
description: 'Replace the given primary with the indicated replica.', | ||
handler: function (argv) { | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Replacing primary ${continuum.db1} with ${continuum.db2}...`) | ||
getConsent().then((consent) => { | ||
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...') | ||
return continuum.replacePrimary().then(() => { | ||
log('... success!') | ||
}) | ||
}).catch(catchError) | ||
await continuum.replacePrimary() | ||
console.log(`Successfully replaced ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
} | ||
@@ -141,38 +126,39 @@ }) | ||
description: 'Migrate all non-special databases to new settings.', | ||
handler: function (argv) { | ||
const { couchUrl, filterTombstones, interval, placement, q, verbose } = argv | ||
handler: async function (argv) { | ||
const { couchUrl, verbose } = argv | ||
if (verbose) { process.env.LOG = true } | ||
CouchContinuum | ||
.getCheckpoint(couchUrl) | ||
.then((dbNames) => { | ||
return dbNames.map((dbName) => { | ||
const options = { couchUrl, dbName, filterTombstones, interval, placement, q } | ||
return new CouchContinuum(options) | ||
}) | ||
try { | ||
const dbNames = await CouchContinuum.getRemaining(couchUrl) | ||
const continuums = dbNames.map((dbName) => { | ||
return new CouchContinuum({ dbName, ...argv }) | ||
}) | ||
.then((continuums) => { | ||
log('Creating replicas...') | ||
return CouchContinuum | ||
.createReplicas(continuums) | ||
.then(() => { | ||
return getConsent('Ready to replace primaries with replicas. Continue? [y/N] ') | ||
}) | ||
.then((consent) => { | ||
if (!consent) return log('Could not acquire consent. Exiting...') | ||
log('Replacing primaries...') | ||
return CouchContinuum | ||
.replacePrimaries(continuums) | ||
}) | ||
}) | ||
.then(() => { | ||
return CouchContinuum | ||
.removeCheckpoint() | ||
}) | ||
.then(() => { | ||
log('...success!') | ||
}) | ||
.catch(catchError) | ||
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) } | ||
} | ||
}) | ||
// backwards compat with old flag names | ||
.alias('source', 'dbNames') | ||
.alias('source', 'N') | ||
.alias('target', 'copyName') | ||
.alias('target', 'c') | ||
// actual options | ||
.options({ | ||
source: { | ||
alias: 's', | ||
description: 'The name or URL of a database to use as a primary.', | ||
required: true, | ||
type: 'string' | ||
}, | ||
target: { | ||
alias: 't', | ||
description: 'The name or URL of a database to use as a replica. Defaults to {source}_temp_copy', | ||
type: 'string' | ||
}, | ||
couchUrl: { | ||
@@ -208,4 +194,9 @@ alias: 'u', | ||
alias: 'f', | ||
description: 'Filter tombstones during replica creation.', | ||
description: 'Filter tombstones during replica creation. Does not work with CouchDB 1.x', | ||
default: false | ||
}, | ||
replicateSecurity: { | ||
alias: 'r', | ||
description: 'Replicate a database\'s /_security object in addition to its documents.', | ||
default: true | ||
} | ||
@@ -212,0 +203,0 @@ }) |
521
index.js
@@ -1,45 +0,21 @@ | ||
'use strict' | ||
const assert = require('assert') | ||
const fs = require('fs') | ||
const path = require('path') | ||
const ProgressBar = require('progress') | ||
const request = require('request') | ||
const urlParse = require('url').parse | ||
const log = require('./lib/log') | ||
const request = require('./lib/request') | ||
const { name } = require('./package.json') | ||
const { readFile, unlink, writeFile } = require('./lib/fs') | ||
const checkpoint = path.join(__dirname, '.checkpoint') | ||
const prefix = '[couch-continuum]' | ||
function log () { | ||
if (process.env.DEBUG || process.env.LOG) { | ||
arguments[0] = [prefix, arguments[0]].join(' ') | ||
console.log.apply(console, arguments) | ||
} | ||
} | ||
function makeCallBack (resolve, reject) { | ||
return (err, res, body) => { | ||
if (typeof body === 'string') body = JSON.parse(body) | ||
if (err || body.error) return reject(err || body) | ||
else return resolve(body) | ||
} | ||
} | ||
function makeRequest (options) { | ||
return new Promise((resolve, reject) => { | ||
const done = makeCallBack(resolve, reject) | ||
request(options, done) | ||
}) | ||
} | ||
module.exports = | ||
class CouchContinuum { | ||
static allDbs (url) { | ||
return makeRequest({ | ||
url: [url, '_all_dbs'].join('/'), | ||
json: true | ||
}).then((body) => { | ||
return body.filter((dbName) => { | ||
const isSpecial = (dbName[0] === '_') // ignore special dbs | ||
const isReplica = dbName.indexOf('_temp_copy') > -1 | ||
return !isSpecial && !isReplica | ||
}) | ||
static async allDbs (url) { | ||
const allDbs = await request({ url: `${url}/_all_dbs`, json: true }) | ||
return allDbs.filter((dbName) => { | ||
const isSpecial = (dbName[0] === '_') // ignore special dbs | ||
const isReplica = dbName.indexOf('_temp_copy') > -1 | ||
return !isSpecial && !isReplica | ||
}) | ||
@@ -51,74 +27,99 @@ } | ||
* against a particular CouchDB cluster. | ||
* @param {String} couchUrl Location of the CouchDB cluster | ||
* @return {Promise<Array>} List of databases still to be migrated. | ||
* @return {Array<String>} List of databases still to be migrated. | ||
*/ | ||
static getCheckpoint (couchUrl) { | ||
// skip already done | ||
return CouchContinuum.allDbs(couchUrl).then((dbNames) => { | ||
return new Promise((resolve, reject) => { | ||
fs.readFile(checkpoint, 'utf-8', (err, lastDb) => { | ||
if (err) { | ||
if (err.code === 'ENOENT') return resolve('\u0000') | ||
return reject(err) | ||
} | ||
return resolve(lastDb) | ||
}) | ||
}).then((lastDb) => { | ||
// ignore any databases that sort lower than | ||
// the name in the checkpoint doc, | ||
// or the default: the lowest unicode value | ||
return dbNames.filter((dbName) => { | ||
return dbName > lastDb | ||
}) | ||
}) | ||
static async getCheckpoint () { | ||
return readFile(checkpoint, 'utf-8').catch((error) => { | ||
if (error.code === 'ENOENT') { | ||
return '\u0000' | ||
} else { | ||
throw error | ||
} | ||
}) | ||
} | ||
static makeCheckpoint (dbName) { | ||
return new Promise((resolve, reject) => { | ||
fs.writeFile(checkpoint, dbName, 'utf-8', (err) => { | ||
if (err) return reject(err) | ||
return resolve() | ||
}) | ||
}) | ||
static async makeCheckpoint (dbName) { | ||
await writeFile(checkpoint, dbName, 'utf-8') | ||
} | ||
static removeCheckpoint () { | ||
return new Promise((resolve, reject) => { | ||
fs.unlink(checkpoint, (err) => { | ||
if (err) return reject(err) | ||
return resolve() | ||
}) | ||
static async removeCheckpoint () { | ||
await unlink(checkpoint) | ||
} | ||
static async getRemaining (couchUrl) { | ||
const dbNames = await CouchContinuum.allDbs(couchUrl) | ||
const lastDb = await CouchContinuum.getCheckpoint() | ||
// ignore any databases that sort lower than | ||
// the name in the checkpoint doc. | ||
return dbNames.filter((dbName) => { return dbName > lastDb }) | ||
} | ||
static async createReplicas (continuums) { | ||
for (let continuum of continuums) { | ||
await continuum.createReplica() | ||
} | ||
} | ||
static async replacePrimaries (continuums) { | ||
for (let continuum of continuums) { | ||
await continuum.replacePrimary() | ||
await CouchContinuum.makeCheckpoint(continuum.source.path.slice(1)) | ||
} | ||
await CouchContinuum.removeCheckpoint() | ||
} | ||
static async _isAvailable (dbUrl) { | ||
const { down } = await request({ | ||
url: `${dbUrl}/_local/in-maintenance`, | ||
json: true | ||
}) | ||
return !down | ||
} | ||
static createReplicas (continuums) { | ||
return continuums.map((continuum) => { | ||
return () => { | ||
return continuum.createReplica() | ||
} | ||
}).reduce((a, b) => { | ||
return a.then(b) | ||
}, Promise.resolve()) | ||
static async _setUnavailable (dbUrl) { | ||
await request({ | ||
url: `${dbUrl}/_local/in-maintenance`, | ||
method: 'PUT', | ||
json: { down: true } | ||
}) | ||
} | ||
static replacePrimaries (continuums) { | ||
return continuums.map((continuum) => { | ||
return () => { | ||
log('Replacing primary "%s" with replica "%s"', continuum.db1, continuum.db2) | ||
return continuum.replacePrimary().then(() => { | ||
return CouchContinuum.makeCheckpoint(continuum.db1) | ||
}) | ||
} | ||
}).reduce((a, b) => { | ||
return a.then(b) | ||
}, Promise.resolve()) | ||
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' }) | ||
} | ||
constructor ({ couchUrl, dbName, copyName, filterTombstones, placement, interval, q, n }) { | ||
constructor ({ | ||
couchUrl, | ||
filterTombstones, | ||
interval, | ||
n, | ||
placement, | ||
q, | ||
replicateSecurity, | ||
source, | ||
target | ||
}) { | ||
assert(couchUrl, 'The Continuum requires a URL for accessing CouchDB.') | ||
assert(dbName, 'The Continuum requires a target database.') | ||
this.url = couchUrl | ||
this.db1 = encodeURIComponent(dbName) | ||
this.db2 = (copyName && encodeURIComponent(copyName)) || (this.db1 + '_temp_copy') | ||
assert(source, 'The Continuum requires a source database.') | ||
this.url = urlParse(couchUrl) | ||
// get source url | ||
const parsedSource = urlParse(source) | ||
if (parsedSource.host) { | ||
this.source = parsedSource | ||
} else { | ||
this.source = urlParse(`${this.url.href}${encodeURIComponent(source)}`) | ||
} | ||
// get target url | ||
if (target) { | ||
const parsedTarget = urlParse(target) | ||
if (parsedTarget.host) { | ||
this.target = parsedTarget | ||
} else { | ||
this.target = urlParse(`${this.url.href}${encodeURIComponent(target)}`) | ||
} | ||
} else { | ||
this.target = urlParse(`${this.source.href}_temp_copy`) | ||
} | ||
// save other variables | ||
this.interval = interval || 1000 | ||
@@ -129,21 +130,28 @@ this.q = q | ||
this.filterTombstones = filterTombstones | ||
log('Created new continuum: %j', { | ||
db1: this.db1, | ||
db2: this.db2, | ||
this.replicateSecurity = replicateSecurity | ||
// what's great for a snack and fits on your back | ||
// it's log it's log it's log | ||
// everyone wants a log | ||
log(`Created new continuum: ${JSON.stringify({ | ||
filterTombstones: this.filterTombstones, | ||
interval: this.interval, | ||
n: this.n, | ||
placement: this.placement, | ||
q: this.q, | ||
n: this.n, | ||
placement: this.placement | ||
}) | ||
replicateSecurity: this.replicateSecurity, | ||
source: `${this.source.host}${this.source.path}`, | ||
target: `${this.target.host}${this.target.path}`, | ||
url: this.url.host | ||
}, undefined, 2)}`) | ||
} | ||
_createDb (dbName) { | ||
var qs = {} | ||
if (this.q) qs.q = this.q | ||
if (this.n) qs.n = this.n | ||
if (this.placement) qs.placement = this.placement | ||
return makeRequest({ | ||
url: [this.url, dbName].join('/'), | ||
async _createDb (dbUrl) { | ||
const qs = {} | ||
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: qs, | ||
qs, | ||
json: true | ||
@@ -153,5 +161,5 @@ }) | ||
_destroyDb (dbName) { | ||
return makeRequest({ | ||
url: [this.url, dbName].join('/'), | ||
async _destroyDb (dbUrl) { | ||
return request({ | ||
url: dbUrl, | ||
method: 'DELETE', | ||
@@ -162,80 +170,56 @@ json: true | ||
_replicate (source, target, selector) { | ||
return makeRequest({ | ||
url: [this.url, source].join('/'), | ||
json: true | ||
}).then((body) => { | ||
const total = body.doc_count | ||
if (total === 0) return Promise.resolve() | ||
console.log('[couch-continuum] Replicating %s to %s', source, target) | ||
const text = '[couch-continuum] (:bar) :percent :etas' | ||
const bar = new ProgressBar(text, { | ||
incomplete: ' ', | ||
width: 20, | ||
total | ||
async _replicate (source, target, selector) { | ||
const { doc_count: total } = await request({ url: source, json: true }) | ||
if (total === 0) return null | ||
console.log(`[${name}] Replicating ${target.host}${source.path} to ${target.host}${target.path}`) | ||
const text = `[${name}] (:bar) :percent :etas` | ||
const bar = new ProgressBar(text, { | ||
incomplete: ' ', | ||
width: 20, | ||
total | ||
}) | ||
var current = 0 | ||
const timer = setInterval(async () => { | ||
const { doc_count: latest } = await request({ | ||
url: target.href, | ||
json: true | ||
}) | ||
var current = 0 | ||
const timer = setInterval(() => { | ||
makeRequest({ | ||
url: [this.url, target].join('/'), | ||
json: true | ||
}).then((body) => { | ||
const latest = body.doc_count | ||
const delta = latest - current | ||
bar.tick(delta) | ||
current = latest | ||
if (bar.complete) clearInterval(timer) | ||
}).catch((error) => { | ||
if (error) console.error(error) | ||
}) | ||
}, this.interval) | ||
return makeRequest({ | ||
url: [this.url, '_replicate'].join('/'), | ||
method: 'POST', | ||
json: { source, target, selector } | ||
}).then(() => { | ||
bar.tick(total) | ||
clearInterval(timer) | ||
}) | ||
const delta = latest - current | ||
bar.tick(delta) | ||
current = latest | ||
if (bar.complete) clearInterval(timer) | ||
// TODO catch errors produced by this loop | ||
}, this.interval) | ||
await request({ | ||
url: `${this.url.href}_replicate`, | ||
method: 'POST', | ||
json: { source: source.href, target: target.href, selector } | ||
}) | ||
} | ||
_verifyReplica () { | ||
const getDocCount = (dbName) => { | ||
return makeRequest({ | ||
url: [this.url, dbName].join('/'), | ||
// copy security object over | ||
if (this.replicateSecurity) { | ||
log(`Replicating ${source}/_security to ${target}...`) | ||
const security = await request({ | ||
url: `${this.source.href}/_security`, | ||
json: true | ||
}).then((body) => { | ||
return body.doc_count | ||
}) | ||
await request({ | ||
url: `${this.target.href}/_security`, | ||
method: 'PUT', | ||
json: security | ||
}) | ||
} | ||
return Promise.all([ | ||
getDocCount(this.db1), | ||
getDocCount(this.db2) | ||
]).then(([docCount1, docCount2]) => { | ||
assert.equal(docCount1, docCount2, 'Primary and replica do not have the same number of documents.') | ||
}) | ||
bar.tick(total) | ||
clearInterval(timer) | ||
} | ||
_setUnavailable () { | ||
return makeRequest({ | ||
url: [this.url, this.db1, '_local', 'in-maintenance'].join('/'), | ||
method: 'PUT', | ||
json: { down: true } | ||
}).catch((error) => { | ||
if (error.error === 'file_exists') return null | ||
else throw error | ||
async _verifyReplica () { | ||
const { doc_count: docCount1 } = await request({ | ||
url: this.source.href, | ||
json: true | ||
}) | ||
} | ||
_setAvailable () { | ||
const url = [this.url, this.db1, '_local', 'in-maintenance'].join('/') | ||
return makeRequest({ url, json: true }).catch((error) => { | ||
if (error.error === 'not_found') return {} | ||
else throw error | ||
}).then(({ _rev }) => { | ||
const qs = _rev ? { rev: _rev } : {} | ||
return makeRequest({ url, qs, method: 'DELETE' }) | ||
const { doc_count: docCount2 } = await request({ | ||
url: this.target.href, | ||
json: true | ||
}) | ||
assert.strictEqual(docCount1, docCount2, 'Primary and replica do not have the same number of documents.') | ||
} | ||
@@ -246,11 +230,7 @@ | ||
* @param {String} dbName Name of the database to check. | ||
* @return {Promise} Resolves with the database's update sequence. | ||
* @return {String} The database's update sequence. | ||
*/ | ||
_getUpdateSeq (dbName) { | ||
return makeRequest({ | ||
url: [this.url, dbName].join('/'), | ||
json: true | ||
}).then((body) => { | ||
return body.update_seq | ||
}) | ||
async _getUpdateSeq (dbUrl) { | ||
const { update_seq: updateSeq } = await request({ url: dbUrl, json: true }) | ||
return updateSeq | ||
} | ||
@@ -262,29 +242,19 @@ | ||
* @param {String} dbName Name of the database to check. | ||
* @return {Promise<Boolean>} Whether the database is in use. | ||
* @return {Boolean} Whether the database is in use. | ||
*/ | ||
_isInUse (dbName) { | ||
return Promise.all([ | ||
makeRequest({ | ||
url: [this.url, '_active_tasks'].join('/'), | ||
json: true | ||
}), | ||
makeRequest({ | ||
url: [this.url, '_scheduler', 'jobs'].join('/'), | ||
json: true | ||
}).catch((error) => { | ||
// catch 1.x | ||
if (error.error === 'illegal_database_name') { | ||
return { jobs: [] } | ||
} else { | ||
throw error | ||
} | ||
}) | ||
]).then(([activeTasks, jobsResponse]) => { | ||
const { jobs } = jobsResponse | ||
// verify that the given dbName is not involved | ||
// in any active jobs or tasks | ||
jobs.concat(activeTasks).forEach(({ database }) => { | ||
assert.notEqual(database, dbName, `${dbName} is still in use.`) | ||
}) | ||
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 (let { database } of [...jobs, ...activeTasks]) { | ||
assert.notEqual(database, dbName, `${dbName} is still in use.`) | ||
} | ||
} | ||
@@ -297,83 +267,60 @@ | ||
*/ | ||
createReplica () { | ||
let lastSeq1, lastSeq2 | ||
log(`Creating replica ${this.db2}...`) | ||
async createReplica () { | ||
log(`Creating replica ${this.target.host}${this.target.path}...`) | ||
log('[0/5] Checking if primary is in use...') | ||
return this._isInUse(this.db1).then(() => { | ||
return this._getUpdateSeq(this.db1).then((seq) => { | ||
lastSeq1 = seq | ||
}) | ||
}).then(() => { | ||
log('[1/5] Creating replica db:', this.db2) | ||
return this._createDb(this.db2).catch((err) => { | ||
const exists = (err.error && err.error === 'file_exists') | ||
if (exists) return true | ||
else throw err | ||
}) | ||
}).then(() => { | ||
log('[2/5] Beginning replication of primary to replica...') | ||
var selector | ||
if (this.filterTombstones) selector = { _deleted: { '$exists': false } } | ||
return this._replicate(this.db1, this.db2, selector) | ||
}).then(() => { | ||
log('[3/5] Verifying primary did not change during replication...') | ||
return this._getUpdateSeq(this.db1).then((seq) => { | ||
lastSeq2 = seq | ||
assert(lastSeq1 <= lastSeq2, `${this.db1} is still receiving updates. Exiting...`) | ||
}) | ||
}).then(() => { | ||
log('[4/5] Verifying primary and replica match...') | ||
return this._verifyReplica() | ||
}).then(() => { | ||
log('[5/5] Primary copied to replica.') | ||
}) | ||
await this._isInUse(this.source.path.slice(1)) | ||
const lastSeq1 = await this._getUpdateSeq(this.source.href) | ||
log(`[1/5] Creating replica db: ${this.target.host}${this.target.path}`) | ||
await this._createDb(this.target.href) | ||
log('[2/5] Beginning replication of primary to replica...') | ||
const selector = this.filterTombstones ? { | ||
_deleted: { $exists: false } | ||
} : undefined | ||
await this._replicate(this.source, this.target, selector) | ||
log('[3/5] Verifying primary did not change during replication...') | ||
const lastSeq2 = await this._getUpdateSeq(this.source.href) | ||
assert(lastSeq1 <= lastSeq2, `${this.source.host}${this.source.path} is still receiving updates. Exiting...`) | ||
log('[4/5] Verifying primary and replica match...') | ||
await this._verifyReplica() | ||
log('[5/5] Primary copied to replica.') | ||
} | ||
replacePrimary () { | ||
log(`Replacing primary ${this.db1}...`) | ||
async replacePrimary () { | ||
log(`Replacing primary ${this.source.host}${this.source.path} using ${this.target.host}${this.target.path}...`) | ||
log('[0/8] Checking if primary is in use...') | ||
return this._isInUse(this.db1).then(() => { | ||
log('[1/8] Verifying primary and replica match...') | ||
return this._verifyReplica() | ||
}).then(() => { | ||
log('[2/8] Destroying primary...') | ||
return this._destroyDb(this.db1) | ||
}).then(() => { | ||
log('[3/8] Recreating primary with new settings...') | ||
return this._createDb(this.db1).then(() => { | ||
return new Promise((resolve) => { | ||
// sleep, giving the cluster a chance to sort | ||
// out the rapid recreation. | ||
console.log('[couch-continuum] Recreating primary %s', this.db1) | ||
const text = '[couch-continuum] (:bar) :percent :etas' | ||
const bar = new ProgressBar(text, { | ||
incomplete: ' ', | ||
width: 20, | ||
total: 150 | ||
}) | ||
const timer = setInterval(() => { | ||
bar.tick() | ||
if (bar.complete) { | ||
clearInterval(timer) | ||
return resolve() | ||
} | ||
}, 100) | ||
}) | ||
await this._isInUse(this.source.path.slice(1)) | ||
log('[1/8] Verifying primary and replica match...') | ||
await this._verifyReplica() | ||
log('[2/8] Destroying primary...') | ||
await this._destroyDb(this.source.href) | ||
log('[3/8] Recreating primary with new settings...') | ||
await this._createDb(this.source.href) | ||
await new Promise((resolve) => { | ||
// sleep, giving the cluster a chance to sort | ||
// out the rapid recreation. | ||
console.log(`[${name}] Recreating primary ${this.source.host}${this.source.path}`) | ||
const text = `[${name}] (:bar) :percent :etas` | ||
const bar = new ProgressBar(text, { | ||
incomplete: ' ', | ||
width: 20, | ||
total: 150 | ||
}) | ||
}).then(() => { | ||
log('[4/8] Setting primary to unavailable.') | ||
return this._setUnavailable() | ||
}).then(() => { | ||
log('[5/8] Beginning replication of replica to primary...') | ||
return this._replicate(this.db2, this.db1) | ||
}).then(() => { | ||
log('[6/8] Replicated. Destroying replica...') | ||
return this._destroyDb(this.db2) | ||
}).then(() => { | ||
log('[7/8] Setting primary to available.') | ||
return this._setAvailable() | ||
}).then(() => { | ||
log('[8/8] Primary migrated to new settings.') | ||
const timer = setInterval(() => { | ||
bar.tick() | ||
if (bar.complete) { | ||
clearInterval(timer) | ||
return resolve() | ||
} | ||
}, 100) | ||
}) | ||
log('[4/8] Setting primary to unavailable.') | ||
await CouchContinuum._setUnavailable(this.source.href) | ||
log('[5/8] Beginning replication of replica to primary...') | ||
await this._replicate(this.target, this.source) | ||
log('[6/8] Replicated. Destroying replica...') | ||
await this._destroyDb(this.target.href) | ||
log('[7/8] Setting primary to available.') | ||
await CouchContinuum._setAvailable(this.source.href) | ||
log('[8/8] Primary migrated to new settings.') | ||
} | ||
} |
{ | ||
"name": "couch-continuum", | ||
"version": "2.0.1-alpha", | ||
"version": "2.1.0-alpha", | ||
"description": "Tool for migrating CouchDB databases to new configuration values.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -87,18 +87,24 @@ # couch-continuum | ||
Options: | ||
--version Show version number [boolean] | ||
--couchUrl, -u The URL of the CouchDB cluster to act upon. | ||
--version Show version number [boolean] | ||
--source, --dbNames, -N, -s The name or URL of a database to use as a | ||
primary. [string] [required] | ||
--target, --copyName, -c, -t The name or URL of a database to use as a | ||
replica. Defaults to {source}_temp_copy [string] | ||
--couchUrl, -u The URL of the CouchDB cluster to act upon. | ||
[default: "http://admin:password@localhost:5984"] | ||
--interval, -i How often (in milliseconds) to check replication tasks | ||
for progress. [default: 1000] | ||
-q The desired "q" value for the new database. [number] | ||
-n The desired "n" value for the new database. [number] | ||
--verbose, -v Enable verbose logging. [boolean] | ||
--placement, -p Placement rule for the affected database(s). [string] | ||
--filterTombstones, -f Filter tombstones during replica creation. | ||
[default: false] | ||
--config Path to JSON config file | ||
--dbName, -N The name of the database to modify.[string] [required] | ||
--copyName, -c The name of the database to use as a replica. Defaults | ||
to {dbName}_temp_copy [string] | ||
-h, --help Show help [boolean] | ||
--interval, -i How often (in milliseconds) to check replication | ||
tasks for progress. [default: 1000] | ||
-q The desired "q" value for the new database. | ||
[number] | ||
-n The desired "n" value for the new database. | ||
[number] | ||
--verbose, -v Enable verbose logging. [boolean] | ||
--placement, -p Placement rule for the affected database(s). | ||
[string] | ||
--filterTombstones, -f Filter tombstones during replica creation. Does | ||
not work with CouchDB 1.x [default: false] | ||
--replicateSecurity, -r Replicate a database's /_security object in | ||
addition to its documents. [default: false] | ||
--config Path to JSON config file | ||
-h, --help Show help [boolean] | ||
``` | ||
@@ -109,8 +115,14 @@ | ||
``` | ||
$ couch-continuum -N hello-world -q 4 -u http://... -v | ||
[couch-continuum] Created new continuum: {"db1":"hello-world","db2":"hello-world_temp_copy","interval":1000,"q":4,"n":1} | ||
[couch-continuum] Migrating database 'hello-world'... | ||
[couch-continuum] Creating replica hello-world_temp_copy... | ||
$ couch-continuum -s hello-world -q 4 -u https://... -v | ||
[couch-continuum] Created new continuum: { | ||
"url": "localhost:5984", | ||
"source": "localhost:5984/hello-world", | ||
"target": "localhost:5984/hello-world_temp_copy", | ||
"interval": 1000, | ||
"q": 4 | ||
} | ||
[couch-continuum] Migrating database: localhost:5984/hello-world | ||
[couch-continuum] Creating replica localhost:5984/hello-world_temp_copy... | ||
[couch-continuum] [0/5] Checking if primary is in use... | ||
[couch-continuum] [1/5] Creating replica db: hello-world_temp_copy | ||
[couch-continuum] [1/5] Creating replica db: localhost:5984/hello-world_temp_copy | ||
[couch-continuum] [2/5] Beginning replication of primary to replica... | ||
@@ -121,3 +133,3 @@ [couch-continuum] [3/5] Verifying primary did not change during replication... | ||
Ready to replace the primary with the replica. Continue? [y/N] y | ||
[couch-continuum] Replacing primary hello-world... | ||
[couch-continuum] Replacing primary localhost:5984/hello-world using localhost:5984/hello-world_temp_copy... | ||
[couch-continuum] [0/8] Checking if primary is in use... | ||
@@ -127,3 +139,3 @@ [couch-continuum] [1/8] Verifying primary and replica match... | ||
[couch-continuum] [3/8] Recreating primary with new settings... | ||
[couch-continuum] Recreating primary hello-world | ||
[couch-continuum] Recreating primary localhost:5984/hello-world | ||
[couch-continuum] (====================) 100% 0.0s | ||
@@ -135,3 +147,3 @@ [couch-continuum] [4/8] Setting primary to unavailable. | ||
[couch-continuum] [8/8] Primary migrated to new settings. | ||
[couch-continuum] ... success! | ||
Migrated database: localhost:5984/hello-world | ||
``` | ||
@@ -138,0 +150,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45997
12
170
677
1