0.2.11 refactored consumer connect/reconnect to be more robust

emits both
also keeps track of ready state
tls
David Kebler 2019-04-11 20:57:45 -07:00
parent 1c3c4383c8
commit 81bb898ab4
6 changed files with 111 additions and 90 deletions

View File

@ -7,3 +7,4 @@ yarn.lock
travis.yml travis.yml
.eslintrc.js .eslintrc.js
archive/ archive/
docs/

View File

@ -15,12 +15,11 @@ client.registerPacketProcessor(process)
; ;
(async () => { (async () => {
// await Promise.all([client1.connect(),client2.connect()]) // await Promise.all([client1.connect(),client2.connect()])
await client.connect() await client.connect()
console.log('sending packet ', packet)
console.log('=========\n',await client.send(packet)) console.log('=========\n',await client.send(packet))
client.end() // client.end()
})().catch(err => { })().catch(err => {
console.error('FATAL: UNABLE TO START SYSTEM!\n',err) console.error('FATAL: UNABLE TO START SYSTEM!\n',err)

View File

@ -1,22 +1,22 @@
import { Socket as uSocket, sSocket} from '../src' import { Socket as uSocket} from '../src'
import { fs } from 'mz' // import { fs } from 'mz'
// made key cert into module that also uses environment variables // made key cert into module that also uses environment variables
const TLS = process.env.TLS || false // const TLS = process.env.TLS || false
const TLS_DIR = process.env.TLS_DIR || '/opt/certs' // const TLS_DIR = process.env.TLS_DIR || '/opt/certs'
const TLS_NAME = process.env.TLD_NAME || 'wc.kebler.net' // const TLS_NAME = process.env.TLD_NAME || 'wc.kebler.net'
const TLS_KEY_PATH = process.env.TLS_KEY_PATH || `${TLS_DIR}/${TLS_NAME}.key` // const TLS_KEY_PATH = process.env.TLS_KEY_PATH || `${TLS_DIR}/${TLS_NAME}.key`
const TLS_CRT_PATH = process.env.TLS_CRT_PATH || `${TLS_DIR}/${TLS_NAME}.crt` // const TLS_CRT_PATH = process.env.TLS_CRT_PATH || `${TLS_DIR}/${TLS_NAME}.crt`
let Socket = uSocket let Socket = uSocket
; ;
(async () => { (async () => {
// TODO dynamic import // TODO dynamic import
if(TLS_KEY_PATH && TLS_CRT_PATH && TLS) { // if(TLS_KEY_PATH && TLS_CRT_PATH && TLS) {
Socket = sSocket // Socket = sSocket
console.log('using TLS') // console.log('using TLS')
} // }
class Test extends Socket { class Test extends Socket {
constructor(opts) { constructor(opts) {
@ -44,22 +44,23 @@ let Socket = uSocket
} }
const options = { // const options = {
tls: TLS, // tls: TLS,
key: await fs.readFile(TLS_KEY_PATH), // key: await fs.readFile(TLS_KEY_PATH),
cert: await fs.readFile(TLS_CRT_PATH), // cert: await fs.readFile(TLS_CRT_PATH),
// This is necessary only if using client certificate authentication. // // This is necessary only if using client certificate authentication.
// requestCert: true, // // requestCert: true,
// This is necessary only if the client uses a self-signed certificate. // // This is necessary only if the client uses a self-signed certificate.
// ca: [ fs.readFileSync('client-cert.pem') ] // // ca: [ fs.readFileSync('client-cert.pem') ]
} // }
options.path = true let options = {path:true}
// let test = new Test() // let test = new Test()
let test = new Test(options) let test = new Test(options)
await test.create() await test.create()
console.log('ready')
})().catch(err => { })().catch(err => {
console.error('FATAL: UNABLE TO START SYSTEM!\n',err) console.error('FATAL: UNABLE TO START SYSTEM!\n',err)

View File

@ -1,6 +1,6 @@
{ {
"name": "@uci/socket", "name": "@uci/socket",
"version": "0.2.10", "version": "0.2.11",
"description": "JSON packet intra(named)/inter(TCP) host communication over socket", "description": "JSON packet intra(named)/inter(TCP) host communication over socket",
"main": "src", "main": "src",
"scripts": { "scripts": {
@ -8,10 +8,10 @@
"test": "mocha -r esm --timeout 10000 test/*.test.mjs", "test": "mocha -r esm --timeout 10000 test/*.test.mjs",
"testlog": "UCI_DEV=true mocha -r esm --timeout 10000 test/*.test.mjs", "testlog": "UCI_DEV=true mocha -r esm --timeout 10000 test/*.test.mjs",
"testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true", "testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true",
"s": "UCI_DEV=true node -r esm examples/server", "s": "UCI_ENV=dev node -r esm examples/server",
"sp": "UCI_DEV=true node -r esm examples/server-push", "sp": "UCI_DEV=true node -r esm examples/server-push",
"devs": "SOCKETS_DIR=/opt/sockets UCI_DEV=true ./node_modules/.bin/nodemon -r esm-e mjs examples/server", "devs": "SOCKETS_DIR=/opt/sockets UCI_DEV=true ./node_modules/.bin/nodemon -r esm-e mjs examples/server",
"c": "UCI_DEV=true node -r esm examples/client", "c": "UCI_ENV=dev node -r esm examples/client",
"cp": "UCI_DEV=true node -r esm examples/client-push", "cp": "UCI_DEV=true node -r esm examples/client-push",
"devc": "SOCKETS_DIR=/opt/sockets UCI_DEV=true node -r esm examples/client", "devc": "SOCKETS_DIR=/opt/sockets UCI_DEV=true node -r esm examples/client",
"c2": "node -r esm examples/client2" "c2": "node -r esm examples/client2"

View File

@ -45,9 +45,11 @@ class SocketConsumer extends Socket {
opts.path = path.join(DEFAULT_PIPE_DIR, opts.path) opts.path = path.join(DEFAULT_PIPE_DIR, opts.path)
} }
this.opts = opts this.opts = opts
// default is keepAlive true, must set to false to explicitly disable
// if keepAlive is true then consumer will also be reconnecting consumer
this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true
this._ready = false this._ready = false
this.timeout = opts.timeout || 300 // 5 minutes and then rejects this.timeout = opts.timeout || 60 // initial connect timeout in secs and then rejects
this.wait = opts.wait || 2 this.wait = opts.wait || 2
this.stream = new JsonStream() this.stream = new JsonStream()
// bind to class for other class functions // bind to class for other class functions
@ -59,22 +61,11 @@ class SocketConsumer extends Socket {
async connect() { async connect() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const connect = () => {
if (this.opts.host === '127.0.0.1')
log.warn(
'tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead'
)
log.info(
{ opts: this.opts },
`attempting to connect ${this.id} to socket`
)
super.connect(this.opts)
}
let reconnect = {} let initial = true
// this is only for initial connection // this is only for initial connection
const timeout = setTimeout(() => { const initTimeout = setTimeout(() => {
clearTimeout(reconnect)
log.fatal({ opts: this.opts }, `unable to connect in ${this.timeout}s`) log.fatal({ opts: this.opts }, `unable to connect in ${this.timeout}s`)
reject( reject(
{ opts: this.opts }, { opts: this.opts },
@ -82,47 +73,83 @@ class SocketConsumer extends Socket {
) )
}, this.timeout * 1000) }, this.timeout * 1000)
this.once('connect', async () => { this.once('connect', async () => {
clearTimeout(timeout) clearTimeout(initTimeout)
this._listen() this._listen()
log.info( log.info({ opts: this.opts, msg:'initial connect waiting for socket ready handshake'})
{ opts: this.opts }, this.setKeepAlive(this.keepAlive, 3000)
'connected waiting for socket ready handshake' let [err, res] = await btc(isReady).bind(this)(this.__ready,this.wait,this.timeout)
)
this.setKeepAlive(this.keepAlive, 100)
let [err, res] = await btc(isReady).bind(this)(
this.__ready,
this.wait,
this.timeout
)
if (err) reject(err) if (err) reject(err)
log.info('handshake done, authenticating') initial = false
log.info('handshake to socket done, authenticating')
// TODO authenticate here by encrypting a payload with private key and sending that. // TODO authenticate here by encrypting a payload with private key and sending that.
// await btc(authenticate) // await btc(authenticate)
this.emit('connected') // for end users to take action
resolve(res) resolve(res)
}) })
this.on('error', async err => { let reconTimeout
log.warn({ error: err.code }, `connect error ${err.code}`) // function that sets a reconnect timeout
if (err.code === 'EISCONN') { const reconnect = () => {
return resolve('ready') reconTimeout = setTimeout(() => {
} this.removeAllListeners()
this.stream.removeAllListeners()
reconnect = setTimeout(() => { this.destroy()
connect() connect()
}, this.wait * 1000) }, this.wait * 1000)
}) }
this.on('end', async () => {
log.warn('socket (server) terminated unexpectantly') // connection function that sets listeners and deals with reconnect
if (this.keepAlive) { const connect = () => {
log.info( if (this.opts.host === '127.0.0.1'|| this.opts.host ==='localhost')
'keep alive was set, so waiting on server to come online for reconnect' log.warn('tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead')
)
this.destroy() if(!initial) {
this.emit('error', { code: 'DISCONNECTED' }) this.once('connect', async () => {
clearTimeout(reconTimeout)
this._listen()
log.info({msg:'reconnected waiting for socket ready handshake'})
this.setKeepAlive(this.keepAlive, 3000)
let [err, res] = await btc(isReady).bind(this)(this.__ready,this.wait,this.timeout)
if (err) reject(err)
log.info('rehandshake done, reauthenticating')
// TODO authenticate here by encrypting a payload with private key and sending that.
// await btc(authenticate)
this.emit('reconnected') // for end users to take action
resolve(res)
})
} }
})
this.on('error', async err => {
if (err.code !== 'EISCONN') {
this._ready = false
this.emit('ready', false)
log.warn({ error: err.code }, `connect error ${err.code}, attempting reconnect`)
reconnect()
}
else {
this._ready = true
this.emit('ready', true)
log.info('reconnected to socket, ready to go again')
}
})
if (this.keepAlive) { // only attempt reconnect is keepAlive is set which it is by default
this.on('end', async () => {
log.warn('socket (server) terminated unexpectantly')
this._ready = false
log.info('keep alive was set, so waiting on server to come online for reconnect')
this.emit('error', { code: 'DISCONNECTED' })
})
}
// attempt connection
log.info({ opts: this.opts, msg:`attempting to connect ${this.id} to socket`})
super.connect(this.opts)
} // end connect function
connect() // initial connect request connect() // initial connect request
}) //end promise }) //end promise
@ -130,11 +157,9 @@ class SocketConsumer extends Socket {
async send(ipacket) { async send(ipacket) {
return new Promise(async resolve => { return new Promise(async resolve => {
// need this for when multiple sends for different consumers use same packet instance if (!this._ready) resolve({ error: 'socket consumer not connected, aborting send' })
let packet = Object.assign({}, ipacket) let packet = Object.assign({}, ipacket) // need to avoid mutuation for different consumers using same packet instance
setTimeout(() => { setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000)
resolve({ error: 'no response from socket in 10sec' })
}, 10000)
packet._header = { packet._header = {
id: Math.random() id: Math.random()
.toString() .toString()
@ -146,24 +171,20 @@ class SocketConsumer extends Socket {
} }
let [err, res] = await btc(this.stream.serialize)(packet) let [err, res] = await btc(this.stream.serialize)(packet)
if (err) if (err)
resolve({ resolve({error: 'unable to serialize packet for sending',packet: packet})
error: 'unable to serialize packet for sending',
packet: packet
})
await this.__write(res) await this.__write(res)
this.once(packet._header.id, async function(reply) { this.once(packet._header.id, async function(reply) {
let res = await this._packetProcess(reply) let res = await this._packetProcess(reply)
if (!res) { if (!res) { // if packetProcess was not promise
// if process was not promise returning like just logged to console
res = reply res = reply
// log.warn('consumer function was not promise returning further processing may be out of sequence') log.warn('consumer function was not promise returning further processing may be out of sequence')
} }
resolve(res) resolve(res)
}) //end listener }) //end listener
}) })
} }
// TODO register alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks) // TODO register user alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks)
// TODO register authenciation function (set up default) // TODO register authenciation function (set up default)
registerPacketProcessor(func) { registerPacketProcessor(func) {
@ -195,7 +216,7 @@ class SocketConsumer extends Socket {
// TODO do some extra security here? // TODO do some extra security here?
let res = await this._packetProcess(packet) let res = await this._packetProcess(packet)
if (!res) { if (!res) {
// if process was not promise returning like just logged to console // if process was not promise returning then res will be undefined
log.warn('consumer packet processing function was not promise returning') log.warn('consumer packet processing function was not promise returning')
} }
}) })
@ -204,7 +225,7 @@ class SocketConsumer extends Socket {
this.stream.on('message', messageProcess.bind(this)) this.stream.on('message', messageProcess.bind(this))
async function messageProcess(packet) { async function messageProcess(packet) {
// console.log('incoming packet from socket',packet) log.debug('incoming packet from socket',packet)
if (packet._handshake) { if (packet._handshake) {
this._ready = true this._ready = true
return return
@ -217,9 +238,8 @@ class SocketConsumer extends Socket {
// default packet process just a simple console logger. ignores any cmd: prop // default packet process just a simple console logger. ignores any cmd: prop
_packetProcess(packet) { _packetProcess(packet) {
console.log( console.log('default consumer processor -- log packet from socket to console')
'default consumer processor -- log packet from socket to console' console.log('replace by calling .registerPacketProcessor(func) with your function')
)
console.dir(packet) console.dir(packet)
} }
} // end class } // end class

View File

@ -239,7 +239,7 @@ export default function socketClass(Server) {
}) })
} }
// must have a consumer socket bound to use // consumer send, must have a consumer socket bound to use
async _send(packet) { async _send(packet) {
// timeout already set if sockect can't be drained in 10 secs // timeout already set if sockect can't be drained in 10 secs
return new Promise(resolve => { return new Promise(resolve => {