UNPKG

keuss-cli

Version:

CLI interface to keuss (job queues atop mongodb/redis)

427 lines (354 loc) 12.2 kB
#!/usr/bin/env node const async = require ('async'); const _ = require ('lodash'); const rand_obj = require ('random-object'); const Chance = require ('chance'); const util = require ('util'); let logUpdate; let chalk; const chance = new Chance(); const { Command } = require('commander'); const program = new Command(); ///////////////////////////////////////// function out (ctx, ...args) { if (ctx.main_opts.verbose) logUpdate ('-->', ...args); } ///////////////////////////////////////// function create_mq (ctx, cb) { const opts = ctx.main_opts; out (ctx, `MQ.init: using backend ${chalk.blue.bold(opts.backend || 'mongo')}`); logUpdate.done(); const mq_opts = _.clone (opts.backendOpt || {}); if (opts.signaller) { const signal_provider = require ('keuss/signal/' + opts.signaller); mq_opts.signaller = { provider: signal_provider }; out (ctx, `MQ.init: using signaller ${chalk.blue.bold(opts.signaller)}`); logUpdate.done(); } if (opts.stats) { const stats_provider = require ('keuss/stats/' + opts.stats); mq_opts.stats = { provider: stats_provider }; out (ctx, `MQ.init: using stats ${chalk.blue.bold(opts.stats)}`); logUpdate.done(); } const MQ = require ('keuss/backends/' + (opts.backend || 'mongo')); MQ (mq_opts, (err, factory) => { if (err) return cb (err); out (ctx, 'MQ.init: keuss initiated'); logUpdate.done(); ctx.factory = factory; cb (); }); } ///////////////////////////////////////// function select_q (ctx, cb) { ctx.q = ctx.factory.queue (ctx.qname || 'test', ctx.main_opts.queueOpt, (err, q) => { ctx.q = q; cb (err); }); } ///////////////////////////////////////// function pop_loop (ctx, state, cb) { if (state.g.n == 0) return cb (); const next_n = (state.g.n == -1) ? state.g.n : (state.g.n ? state.g.n - 1 : state.g.n); state.g.n = next_n; ctx.q.pop ('keuss-cli', (err, res) => { if (err) { if (err == 'cancel') { logUpdate.done(); out (ctx, chalk.yellow.bold(state.id), 'cancelled, stopping consumer'); logUpdate.done(); return; } return cb (err); } if (ctx.cmd_opts.dumpProduced) console.log ('%j', res); out (ctx, chalk.yellow.bold(state.id), `got (pop) element, ${chalk.bold(state.g.n + '')} to go`); if (ctx.cmd_opts.delay) { setTimeout (() => pop_loop (ctx, state, cb), ctx.cmd_opts.delay); } else { pop_loop (ctx, state, cb); } }); } ///////////////////////////////////////// function rcr_loop (ctx, state, cb) { if (state.g.n == 0) return cb (); const next_n = (state.g.n == -1) ? state.g.n : (state.g.n ? state.g.n - 1 : state.g.n); state.g.n = next_n; ctx.q.pop ('keuss-cli', {reserve: true}, (err, res) => { if (err) { if (err == 'cancel') { logUpdate.done(); out (ctx, chalk.yellow.bold(state.id), 'cancelled, stopping consumer'); logUpdate.done(); return; } } ctx.q.ok (res, err => { if (err) return cb (err); if (ctx.cmd_opts.dumpProduced) console.log ('%j', res); out (ctx, chalk.yellow.bold(state.id), `got (reserve+commit) element, ${chalk.bold(state.g.n + '')} to go`); if (ctx.cmd_opts.delay) { setTimeout (() => rcr_loop (ctx, state, cb), ctx.cmd_opts.delay); } else { rcr_loop (ctx, state, cb); } }); }); } ///////////////////////////////////////// function push_loop (ctx, state, cb) { if (state.g.n == 0) return cb (); const opts = {}; const obj = state.obj || state.pool_of_objs[chance.integer ({min:0, max:110})]; const next_n = (state.g.n == -1) ? state.g.n : (state.g.n ? state.g.n - 1 : state.g.n); state.g.n = next_n; ctx.q.push (obj, opts, (err, res) => { if (err) { if (err == 'drain') { logUpdate.done(); out (ctx, chalk.yellow.bold(state.id), 'queue in drain, stopping producer'); logUpdate.done(); return; } return cb (err); } if (ctx.cmd_opts.dumpProduced) console.log ('%j', obj); out (ctx, chalk.yellow.bold(state.id), `pushed element, ${chalk.bold(state.g.n + '')} to go`); if (ctx.cmd_opts.delay) { setTimeout (() => push_loop (ctx, state, cb), ctx.cmd_opts.delay); } else { push_loop (ctx, state, cb); } }); } ///////////////////////////////////////// function parseXOpts (value, prev) { const arr = value.split (':'); if (arr.length < 2) throw new program.InvalidArgumentError('value must be k:v'); const k = arr.shift(); const v = arr.join (':'); if (!prev) prev = {}; prev[k] = v; return prev; } ///////////////////////////////////////// function farewell_and_good_night (ctx) { if (ctx.in_terminus) return; ctx.in_terminus = true; logUpdate.done(); out (ctx, 'farewell...'); async.series ([ cb => { if (ctx.q) { ctx.q.drain (cb); } else { cb(); } }, cb => {if (ctx.q) ctx.q.cancel (); cb ();}, cb => setTimeout (cb, 100), cb => {if (ctx.factory) ctx.factory.close(); cb ();}, ], err => { if (err) console.error (err); logUpdate.done(); out (ctx, '...and good night'); ctx.in_terminus = false; }); } ///////////////////////////////////////// function prepare_for_termination (ctx, cb) { process.on( 'SIGINT', () => farewell_and_good_night (ctx)); process.on( 'SIGQUIT', () => farewell_and_good_night (ctx)); process.on( 'SIGTERM', () => farewell_and_good_night (ctx)); process.on( 'SIGHUP', () => farewell_and_good_night (ctx)); cb (); } (async () => { logUpdate = (await import('log-update')).default; chalk = (await import('chalk')).default; ///////////////////////////////////////////////////////////////////////////////////////////////////////////// program .version ('1.2.0') .usage ('[options]') .option ('-v, --verbose', 'be verbose') .option ('-O, --queue-opt <opt>', 'option to b added to queue, format k:v', parseXOpts) .option ('-b, --backend <value>', 'use queue backend. defaults to \'mongo\'') .option ('-o, --backend-opt <value>', 'option to b added to backend, format k:v', parseXOpts) .option ('-s, --signaller <value>', 'use signaller backend') .option ('-t, --stats <value>', 'use stats backend'); ////////////////////////////////////////////// program .command ('list') .description ('list queues inside backend') .option ('-d, --details', 'show stats-provided details') .option ('-i, --info', 'run an info command on each queue') .action (function () { const ctx = {main_opts: program.opts(), cmd_opts: this.opts()}; async.series ([ cb => create_mq (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => ctx.factory.list ({full: ctx.cmd_opts.details}, cb), ], (err, res) => { if (err) console.error (err); else console.log (util.inspect(res[2], {depth: null, colors: true})); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program .command ('info') .description ('show info and stats for a queue') .argument ('<queue>', 'queue to operate on') .action (function (queue) { const ctx = {qname: queue, main_opts: program.opts(), cmd_opts: this.opts()}; async.series ([ cb => create_mq (ctx, cb), cb => select_q (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => ctx.q.info (cb), ], (err, res) => { if (err) console.error (err); else console.log (util.inspect(res[3], {depth: null, colors: true})); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program .command ('pause') .description ('pauses a queue') .argument ('<queue>', 'queue to operate on') .action (function (queue) { const ctx = {qname: queue, main_opts: program.opts(), cmd_opts: this.opts()}; async.series ([ cb => create_mq (ctx, cb), cb => select_q (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => {ctx.q.pause (true); cb(); }, cb => setTimeout (cb, 100), cb => ctx.q.paused (cb) ], (err, res) => { if (err) console.error (err); else console.log (res[5]); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program .command ('resume') .description ('resumes a queue') .argument ('<queue>', 'queue to operate on') .action (function (queue) { const ctx = {qname: queue, main_opts: program.opts(), cmd_opts: this.opts()}; async.series ([ cb => create_mq (ctx, cb), cb => select_q (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => {ctx.q.pause (false); cb(); }, cb => setTimeout (cb, 100), cb => ctx.q.paused (cb) ], (err, res) => { if (err) console.error (err); else console.log (res[5]); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program .command ('pop') .description ('consumes (pops) from a queue') .argument ('<queue>', 'queue to operate on') .option ('-c, --count <n>', 'number of elements to consume, -1 for infinite', parseInt) .option ('-p, --parallel <n>', 'number of consumers', parseInt) .option ('-d, --delay <ms>', 'delay at the end of each loop cycle, im millisecs', parseInt) .option ('-D, --dump-produced', 'dump text of produced messages to stdout') .option ('-R, --reserve', 'doa reserve+commit instead of a pop') .action (function (queue) { const ctx = {qname: queue, main_opts: program.opts(), cmd_opts: this.opts()}; const global_state = { n: ctx.cmd_opts.count || -1 }; const loops = []; for (let c = 0; c < (ctx.cmd_opts.parallel || 1); c++) { loops.push (cb => { const state = { g: global_state, id: `loop#${c}`, }; out (ctx, `pop: initiating pop loop #${c}`); logUpdate.done(); if (ctx.cmd_opts.reserve) { rcr_loop (ctx, state, cb); } else { pop_loop (ctx, state, cb); } }); } async.series ([ cb => create_mq (ctx, cb), cb => select_q (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => async.parallel (loops, cb), ], (err, res) => { if (err) console.error (err); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program .command ('push') .description ('produces (pushes) to a queue') .argument ('<queue>', 'queue to operate on') .option ('-c, --count <n>', 'number of elements to produce, -1 for infinite', parseInt) .option ('-p, --parallel <n>', 'number of producers', parseInt) .option ('-d, --delay <ms>', 'delay at the end of each loop cycle, im millisecs', parseInt) .option ('-D, --dump-produced', 'dump text of produced messages to stdout') .option ('-J, --object <value>', 'dump text of produced messages to stdout', JSON.parse) .action (function (queue) { const ctx = {qname: queue, main_opts: program.opts(), cmd_opts: this.opts()}; const global_state = { n: ctx.cmd_opts.count || -1 }; const loops = []; for (let c = 0; c < (ctx.cmd_opts.parallel || 1); c++) { loops.push (cb => { const state = { g: global_state, id: `loop#${c}`, }; if (ctx.cmd_opts.object) { state.obj = ctx.cmd_opts.object; } else { // prepare pool of random objects state.pool_of_objs = []; for (let i = 0; i < 111; i++) state.pool_of_objs.push (rand_obj.randomObject ()); } out (ctx, `push: initiating push loop #${c}`); logUpdate.done(); push_loop (ctx, state, cb); }); } async.series ([ cb => create_mq (ctx, cb), cb => select_q (ctx, cb), cb => prepare_for_termination (ctx, cb), cb => async.parallel (loops, cb), ], err => { if (err) console.error (err); farewell_and_good_night (ctx); }); }); ////////////////////////////////////////////// program.parse (process.argv); })()