Skip to content

Commit

Permalink
0.2.0 resilitient master process architecture change
Browse files Browse the repository at this point in the history
  • Loading branch information
haraldrudell committed Jan 10, 2013
1 parent fc9cb49 commit 887161a
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 72 deletions.
10 changes: 6 additions & 4 deletions app.js
Expand Up @@ -40,12 +40,14 @@ function processUncaughtExceptionListener() {
log(processName, 'uncaughtException')
var text = []
Array.prototype.slice.call(arguments).forEach(function (value, index) {
var line = []
var valuePoints = []
var type = typeof value
if (value && value.constructor && value.constructor.name) type += ':' + value.constructor.name
line.push('arg#:', index, 'type:', type, 'value:', value)
if (value && value.stack) line.push('stack:', value.stack)
text.push(line.join(' '))
valuePoints.push(['arg#:', index].join(' '))
valuePoints.push(['type:', type].join(' '))
valuePoints.push(['value:', value].join(' '))
if (value && value.stack) valuePoints.push(['stack:', value.stack].join(' '))
text.push(valuePoints.join(', '))
})
log(text.join('\n'))
}
Expand Down
27 changes: 21 additions & 6 deletions lib/master/rotatedlogger.js
Expand Up @@ -15,22 +15,32 @@ init, log, write
].forEach(function (f) {exports[f.name] = f})

var writeStreamFlags = {
flags: 'a', encoding: 'utf-8', mode: 0660,
flags: 'a', encoding: 'utf-8', mode: 0660, // rw- rw- ---
}
var logSubfolder = 'log'
var logSubfolder = 'log' // subfolder of user's home folder
var logFileExt = '.log'
var logFile = 'applog'
var logFile = 'applog' // default log file basename
var rotateSchedule = 'month'
var logToFile // boolean default false

var logToFile
// flags for output error occured
var writeStreamIsError
var stdoutIsError

var logToFileEnabled // affected by writeStreamIsError and logToFile
var logFilename
var writeStream
var stdoutIsError
var periodTimer
var streamBuffer

/*
Configure rotatedlogger
opts: optional object
.writeStreamFlags: optional object: custom second argument to createWriteStream for the log file
.logSubfolder: optional string subfolder of user's home folder for log files, null for logFile being full qualified filename
.logFile: optional string set basename or full name of log file
.logToFile: optional boolean default false: enable logging to file
*/
function init(opts) {
var result = {
writeStreamFlags: {},
Expand Down Expand Up @@ -74,7 +84,12 @@ function write(str) {
if (streamBuffer) streamBuffer.push(str)
else {
if (!writeStream) openStream()
writeStream.write(str)
try {
writeStream.write(str)
} catch (e) { // error written by stream error listener
writeStreamIsError = true
logToFileEnabled = false
}
}
}
}
Expand Down
58 changes: 30 additions & 28 deletions lib/pidlink.js
Expand Up @@ -8,7 +8,6 @@ var greatjson = require('greatjson')
var fs = require('fs')
// http://nodejs.org/api/path.html
var path = require('path')
if (!fs.exists) fs.exists = path.exists

;[
init, getData
Expand All @@ -23,18 +22,19 @@ var signal = 'SIGUSR2'
var folder
var log = console.log

// this file is not an api, though we can have apprunner monitor errors
var require = require('apprunner').getRequire(require, null, {api: 'pidlink',})
// get error emitter without being an api
var r = require; var require = require('apprunner').getRequire(r, null, {emScope: 'pidlink',})

/*
configure this api
folder0: optional string: fully qualified pathname to a folder where pid files will be read and written
signal0: optional string, default 'SIGURS2' the signal sent to pid
initialWait0: optional number, default 2,000 ms: the time to wait before sending signal
waitTime0: optional number, default 500 ms: the time to wait between signal and file read
log: function to be used for printing
the responder is in the apprunner module's appshutdown file.
Configure pidlink
opts: optional object
.folder: optional string: fully qualified pathname to a folder where pid files will be read and written
.signal: optional string, default 'SIGURS2' the signal sent to pid
.initialWait: optional number, default 2,000 ms: the time to wait before sending signal
.waitTime: optional number, default 500 ms: the time to wait between signal and file read
.log: optional function used for printing
The signal handler in the other process is the apprunner module's appshutdown file.
*/
function init(opts) {
var result = {
Expand All @@ -56,14 +56,16 @@ function init(opts) {
/*
Get data from another process
pid: positive number: process id for the other process
slogan: optional string decsribing the pid process
slogan: optional string decsribing the other process
cb(err, result) function, result: object from json parsing
Wait for 2 seconds to allow the other app time to start properly
send a signal to the process and it will write a file to the filesystem
wait 0.5 seconds to read the result
best effort: if process can't be found or non-init, return undefined result
tmpfolder is used, files are named using the pid with '.json' extension
Send a signal to the process causing it to write a file to the filesystem
Wait 0.5 seconds to read the result
Return data from the file
If process can't be found or does not write the file, reult has undefined value
Files are in tmpfolder, named using the process id number and a '.json' extension
*/
function getData(pid, slogan, cb) {
var result
Expand Down Expand Up @@ -101,30 +103,30 @@ function getData(pid, slogan, cb) {
fs.readFile(file, readResult)
}

function readResult(err, data) {
if (!err) {
function readResult(e, data) {
if (!e) {
var obj = greatjson.parse(data)
if (!(obj instanceof Error)) {
result = obj
fs.unlink(file, end)
} else end(obj)
if (!(obj instanceof Error)) result = obj
else err = obj // saving parsing trouble
fs.unlink(file, end)
} else {
if (err.code == 'ENOENT') {
if (e.code == 'ENOENT') {
log('No data written by:', otherProcess)
end()
} else end(err)
} else end(e)
}
}

function end(err) {
if (!err) cb(null, result)
function end(e) {
if (!e && err) e = err // pick up saved error
if (!e) cb(null, result) // good return
else {
require.emitter.emit.apply(require.emitter, ['error', err, {
require.emitter.emit.apply(require.emitter, ['error', e, {
invocation: invocation,
pid: pid,
slogan: slogan,
}].concat(Array.prototype.slice.call(arguments).slice(1)))
cb(err)
cb(e) // bad return
}
}
}
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -8,7 +8,7 @@
"node",
"operations"
],
"version": "0.1.5",
"version": "0.2.0",
"contributors": [
{
"name": "Harald Rudell",
Expand Down
20 changes: 12 additions & 8 deletions test/test-app.js
Expand Up @@ -2,8 +2,11 @@
// © Harald Rudell 2012

var processfinder = require('../lib/master/processfinder')

var uimanager = require('../lib/master/uimanager')
var rotatedlogger = require('../lib/master/rotatedlogger')
// http://nodejs.org/api/path.html
var path = require('path')

// https://github.com/haraldrudell/mochawrapper
var assert = require('mochawrapper')
Expand All @@ -13,16 +16,14 @@ var sru = processfinder.setResetUi
var ipm = processfinder.isProcessMaster
var lui = uimanager.launchUi
var gur = uimanager.getUiRelauncher

var lg = rotatedlogger.log

exports['App:'] = {
'IsNotMaster ProcessException SIGINT IsMaster': function() {


// execute is not master
var aOn = {}
var eOn = ['uncaughtException', 'SIGUSR2', 'SIGINT']
var eOn = ['uncaughtException', 'SIGUSR2', 'SIGINT', 'SIGHUP']
process.on = function (e, f) {aOn[e] = f; return this}
var aIsProcessMaster = []
var eIsProcessMaster = [[{port: 1113, interface: '127.0.0.1', processName: 0}, 'cb']]
Expand Down Expand Up @@ -51,23 +52,26 @@ exports['App:'] = {

// execute is master
var masterResult = aIsProcessMaster[0][1]

process.on = function () {}
processfinder.isProcessMaster = function (o, cb) {cb(true)}

var uiRelauncher = 5
uimanager.getUiRelauncher = function () {return uiRelauncher}

var aReset = []
var eReset = [uiRelauncher]
processfinder.setResetUi = function (f) {aReset.push(f)}

var aLaunchUi = []
var eLaunchUi = [{uiModuleName: 'webprocess'}]
var eLaunchUi = [{processName: 'x', launchArray: ['node', path.join(__dirname, '..', 'webprocess')], log: 'x'}]
uimanager.launchUi = function (opts) {aLaunchUi.push(opts)}
masterResult(true)

assert.deepEqual(aReset, eReset)
assert.equal(aLaunchUi.length, 1)
assert.equal(typeof aLaunchUi[0].log, 'function')
assert.equal(typeof aLaunchUi[0].processName, 'string')
assert.equal(aLaunchUi[0].uiModuleName, 'webprocess')
assert.equal(typeof (eLaunchUi[0].processName = aLaunchUi[0] && aLaunchUi[0].processName), 'string')
assert.equal(typeof (eLaunchUi[0].log = aLaunchUi[0] && aLaunchUi[0].log), 'function')
assert.deepEqual(aLaunchUi, eLaunchUi)
},
'after': function() {
process.on = on
Expand Down
22 changes: 11 additions & 11 deletions test/test-appconduit.js
Expand Up @@ -56,16 +56,6 @@ exports['AppConduit:'] = {
eData[0].launchTime = aData[0].launchTime
assert.deepEqual(aData, eData)
},
'ReceiveFromUi Launch Non-Array': function() {
// launch non-array
var message = {app: 'APP', launch: true}
var aData = []
var eData = [{error: 'Bad launch command', isConfig: true, message: {app: message.app}}]
appconduit.uiConnect(function (d) {aData.push(d)})
appconduit.receiveFromUi(message)

assert.deepEqual(aData, eData)
},
'ReceiveFromUi Launch': function(done) {

// launch
Expand All @@ -84,7 +74,7 @@ exports['AppConduit:'] = {
var eSpawn = [[
message.launch[0],
[message.launch[1]],
['ignore', 'pipe', 'pipe'],
{detached: true, stdio: ['ignore', 'pipe', 'pipe']},
]]
child_process.spawn = function (command, args, options) {aSpawn.push([command, args, options]); return child}
appconduit.receiveFromUi(message)
Expand Down Expand Up @@ -202,6 +192,16 @@ exports['AppConduit:'] = {
var aMap = appconduit.getState()
assert.deepEqual(aMap, eMap)
},
'ReceiveFromUi Launch Non-Array': function() {
// launch non-array
var message = {app: 'APP', launch: true}
var aData = []
var eData = [{error: 'Bad launch command', isConfig: true, message: {app: message.app}}]
appconduit.uiConnect(function (d) {aData.push(d)})
appconduit.receiveFromUi(message)

assert.deepEqual(aData, eData)
},
'after': function() {
child_process.spawn = sp
process.kill = pk
Expand Down
23 changes: 14 additions & 9 deletions test/test-rotatedlogger.js
Expand Up @@ -22,28 +22,33 @@ exports['RotatedLogger:'] = {
var actual = rotatedlogger.init()
assert.ok(actual)
},
'Write': function () {
var value = '123'
var eValue = value + '\n'
'Write Log': function () {
var value = ['value: %s', 123]
var eValue = 'value: ' + value[1] + '\n'

var aWrite = []
var eWrite = [eValue, eValue]
function mockWrite(x) {aWrite.push(x)}

var aOn = {}
var eOn = ['error']
function mockOn(e, f) {aOn[e] = f; return this}
fs.createWriteStream = function mockCreateWriteStream(x) {return {on: mockOn, write: mockWrite}}

var aTe = 0
perioder.TimeEmitter = function MockTimeEmitter() {this.on = function () {}; aTe++}

var aTOn = []
var eTOn = ['time']
perioder.TimeEmitter = function MockTimeEmitter() {this.on = function (e, f) {aTOn[e] = f}}

process.stdout.write = mockWrite
rotatedlogger.log(value)
rotatedlogger.init({logToFile: true})
rotatedlogger.log(value[0], value[1])
process.stdout.write = wr

assert.deepEqual(aWrite, eWrite)
assert.ok(aTe)
assert.equal(typeof aOn.error, 'function')
assert.deepEqual(Object.keys(aOn).sort(), eOn.sort())
for (var e in aOn) assert.equal(typeof aOn[e], 'function')
assert.deepEqual(Object.keys(aTOn).sort(), eTOn.sort())
for (var e in aTOn) assert.equal(typeof aTOn[e], 'function')
},
'after': function () {
process.stdout.write = wr
Expand Down
10 changes: 5 additions & 5 deletions test/test-uimanager.js
Expand Up @@ -31,7 +31,7 @@ exports['UiManager:'] = {

// launch
var processName = 'PROCESS_NAME'
var uiModuleName = 'UI_MODULE_NAME'
var launchArray = ['EXECUTABLE', 'ARGUMENT']
var aLaunchData = []
var eLaunchData = [[processName, 0]]
appconduit.setLaunchData = function (p, u) {aLaunchData.push([p, u])}
Expand All @@ -42,9 +42,9 @@ exports['UiManager:'] = {
var child = {pid:17, on: onFn, once: onFn, send: childSend}
var aSpawn = []
var eSpawn = [[
'node',
[path.join(path.dirname(require && require.main && require.main.filename), uiModuleName)],
{stdio: ['ignore', 'pipe', 'pipe', 'ipc']},
launchArray[0],
launchArray.slice(1),
{detached: true, stdio: ['ignore', 'pipe', 'pipe', 'ipc']},
]]
child_process.spawn = function mockSpawn(c, a, o) {aSpawn.push([c, a, o]); return child}
var aLog = []
Expand All @@ -54,7 +54,7 @@ exports['UiManager:'] = {
appconduit.uiConnect = function (f) {aUiConnect.push(f)}
var aException = []
var restartIntercept = function (e) {aException.push(e)}
uimanager.launchUi({processName: processName, uiModuleName: uiModuleName, restartIntercept: restartIntercept, log: function () {}})
uimanager.launchUi({processName: processName, launchArray: launchArray, restartIntercept: restartIntercept, log: function () {}})

assert.deepEqual(aException, [], 'launchUi exceptions')
assert.ok(aLaunchData[0])
Expand Down

0 comments on commit 887161a

Please sign in to comment.