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
.eslintrc.js
archive/
docs/

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@uci/socket",
"version": "0.2.10",
"version": "0.2.11",
"description": "JSON packet intra(named)/inter(TCP) host communication over socket",
"main": "src",
"scripts": {
@ -8,10 +8,10 @@
"test": "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",
"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",
"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",
"devc": "SOCKETS_DIR=/opt/sockets UCI_DEV=true node -r esm examples/client",
"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)
}
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._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.stream = new JsonStream()
// bind to class for other class functions
@ -59,22 +61,11 @@ class SocketConsumer extends Socket {
async connect() {
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
const timeout = setTimeout(() => {
clearTimeout(reconnect)
const initTimeout = setTimeout(() => {
log.fatal({ opts: this.opts }, `unable to connect in ${this.timeout}s`)
reject(
{ opts: this.opts },
@ -82,47 +73,83 @@ class SocketConsumer extends Socket {
)
}, this.timeout * 1000)
this.once('connect', async () => {
clearTimeout(timeout)
clearTimeout(initTimeout)
this._listen()
log.info(
{ opts: this.opts },
'connected waiting for socket ready handshake'
)
this.setKeepAlive(this.keepAlive, 100)
let [err, res] = await btc(isReady).bind(this)(
this.__ready,
this.wait,
this.timeout
)
log.info({ opts: this.opts, msg:'initial connect 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('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.
// await btc(authenticate)
this.emit('connected') // for end users to take action
resolve(res)
})
this.on('error', async err => {
log.warn({ error: err.code }, `connect error ${err.code}`)
if (err.code === 'EISCONN') {
return resolve('ready')
}
reconnect = setTimeout(() => {
let reconTimeout
// function that sets a reconnect timeout
const reconnect = () => {
reconTimeout = setTimeout(() => {
this.removeAllListeners()
this.stream.removeAllListeners()
this.destroy()
connect()
}, this.wait * 1000)
})
}
this.on('end', async () => {
log.warn('socket (server) terminated unexpectantly')
if (this.keepAlive) {
log.info(
'keep alive was set, so waiting on server to come online for reconnect'
)
this.destroy()
this.emit('error', { code: 'DISCONNECTED' })
// connection function that sets listeners and deals with reconnect
const connect = () => {
if (this.opts.host === '127.0.0.1'|| this.opts.host ==='localhost')
log.warn('tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead')
if(!initial) {
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
}) //end promise
@ -130,11 +157,9 @@ class SocketConsumer extends Socket {
async send(ipacket) {
return new Promise(async resolve => {
// need this for when multiple sends for different consumers use same packet instance
let packet = Object.assign({}, ipacket)
setTimeout(() => {
resolve({ error: 'no response from socket in 10sec' })
}, 10000)
if (!this._ready) resolve({ error: 'socket consumer not connected, aborting send' })
let packet = Object.assign({}, ipacket) // need to avoid mutuation for different consumers using same packet instance
setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000)
packet._header = {
id: Math.random()
.toString()
@ -146,24 +171,20 @@ class SocketConsumer extends Socket {
}
let [err, res] = await btc(this.stream.serialize)(packet)
if (err)
resolve({
error: 'unable to serialize packet for sending',
packet: packet
})
resolve({error: 'unable to serialize packet for sending',packet: packet})
await this.__write(res)
this.once(packet._header.id, async function(reply) {
let res = await this._packetProcess(reply)
if (!res) {
// if process was not promise returning like just logged to console
if (!res) { // if packetProcess was not promise
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)
}) //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)
registerPacketProcessor(func) {
@ -195,7 +216,7 @@ class SocketConsumer extends Socket {
// TODO do some extra security here?
let res = await this._packetProcess(packet)
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')
}
})
@ -204,7 +225,7 @@ class SocketConsumer extends Socket {
this.stream.on('message', messageProcess.bind(this))
async function messageProcess(packet) {
// console.log('incoming packet from socket',packet)
log.debug('incoming packet from socket',packet)
if (packet._handshake) {
this._ready = true
return
@ -217,9 +238,8 @@ class SocketConsumer extends Socket {
// default packet process just a simple console logger. ignores any cmd: prop
_packetProcess(packet) {
console.log(
'default consumer processor -- log packet from socket to console'
)
console.log('default consumer processor -- log packet from socket to console')
console.log('replace by calling .registerPacketProcessor(func) with your function')
console.dir(packet)
}
} // 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) {
// timeout already set if sockect can't be drained in 10 secs
return new Promise(resolve => {