You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
5.6 KiB
237 lines
5.6 KiB
const dgram = require('dgram')
|
|
const { message } = require('./messages')
|
|
|
|
module.exports = class RPC {
|
|
constructor (opts = {}) {
|
|
this._pendingSends = []
|
|
this._tid = (Math.random() * 65536) | 0
|
|
this._drainInterval = null
|
|
this._tick = 0
|
|
this._w = 0
|
|
this._win = [0, 0, 0, 0]
|
|
|
|
this.maxWindow = opts.maxWindow || 80 // ~100 per second burst, ~80 per second avg
|
|
this.maxRetries = 3
|
|
this.destroyed = false
|
|
this.inflight = []
|
|
this.onrequest = opts.onrequest || noop
|
|
this.onresponse = opts.onresponse || noop
|
|
this.onwarning = opts.onwarning || noop
|
|
this.socket = opts.socket || dgram.createSocket('udp4')
|
|
this.socket.on('message', this.onmessage.bind(this))
|
|
}
|
|
|
|
get inflightRequests () {
|
|
return this.inflight.length
|
|
}
|
|
|
|
send (m, socket = this.socket) {
|
|
const state = { start: 0, end: 0, buffer: null }
|
|
|
|
message.preencode(state, m)
|
|
state.buffer = Buffer.allocUnsafe(state.end)
|
|
message.encode(state, m)
|
|
|
|
this._send(socket, state.buffer, m.to)
|
|
}
|
|
|
|
reply (req, reply, socket = this.socket) {
|
|
reply.tid = req.tid
|
|
reply.to = req.from
|
|
this.send(reply, socket)
|
|
}
|
|
|
|
address () {
|
|
return this.socket.address()
|
|
}
|
|
|
|
bind (port) {
|
|
return new Promise((resolve, reject) => {
|
|
const s = this.socket
|
|
|
|
if (s.listen) {
|
|
s.listen(port)
|
|
} else {
|
|
s.bind(port)
|
|
}
|
|
|
|
s.on('listening', onlistening)
|
|
s.on('error', onerror)
|
|
|
|
function onlistening () {
|
|
s.removeListener('listening', onlistening)
|
|
s.removeListener('error', onerror)
|
|
resolve()
|
|
}
|
|
|
|
function onerror (err) {
|
|
s.removeListener('listening', onlistening)
|
|
s.removeListener('error', onerror)
|
|
reject(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
destroy () {
|
|
if (this.destroyed) return
|
|
this.unwrap(true)
|
|
this.socket.close()
|
|
}
|
|
|
|
unwrap (closing = false) {
|
|
if (this.destroyed) return
|
|
this.destroyed = true
|
|
|
|
clearInterval(this._drainInterval)
|
|
this.socket.removeAllListeners()
|
|
|
|
for (const req of this.inflight) {
|
|
req.reject(new Error('RPC socket destroyed'))
|
|
}
|
|
|
|
this.inflight = []
|
|
return this.socket
|
|
}
|
|
|
|
request (m, opts) {
|
|
if (this.destroyed) return Promise.reject(new Error('RPC socket destroyed'))
|
|
|
|
if (this._drainInterval === null) {
|
|
this._drainInterval = setInterval(this._drain.bind(this), 750)
|
|
if (this._drainInterval.unref) this._drainInterval.unref()
|
|
}
|
|
|
|
m.tid = this._tid++
|
|
if (this._tid === 65536) this._tid = 0
|
|
|
|
const state = { start: 0, end: 0, buffer: null }
|
|
|
|
message.preencode(state, m)
|
|
state.buffer = Buffer.allocUnsafe(state.end)
|
|
message.encode(state, m)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const total = this._win[0] + this._win[1] + this._win[2] + this._win[3]
|
|
const req = {
|
|
socket: (opts && opts.socket) || this.socket,
|
|
timeout: 2,
|
|
expectOk: !!(opts && opts.expectOk !== false),
|
|
tries: (opts && opts.retry === false) ? this.maxRetries : 0,
|
|
tid: m.tid,
|
|
buffer: state.buffer,
|
|
to: m.to,
|
|
resolve,
|
|
reject
|
|
}
|
|
|
|
this.inflight.push(req)
|
|
|
|
if (total < 2 * this.maxWindow && this._win[this._w] < this.maxWindow) {
|
|
this._win[this._w]++
|
|
req.tries++
|
|
this._send(req.socket, req.buffer, req.to)
|
|
}
|
|
})
|
|
}
|
|
|
|
onmessage (buffer, rinfo) {
|
|
const from = { host: rinfo.address, port: rinfo.port }
|
|
if (!from.port) return
|
|
if (buffer.byteLength <= 1) return
|
|
|
|
const state = { start: 0, end: buffer.byteLength, buffer }
|
|
let m = null
|
|
|
|
try {
|
|
m = message.decode(state)
|
|
} catch (err) {
|
|
this.onwarning(err)
|
|
return
|
|
}
|
|
|
|
m.from = from
|
|
|
|
if (m.command !== null) { // request
|
|
if (this.onrequest === noop) return
|
|
this.onrequest(m, this)
|
|
return
|
|
}
|
|
|
|
const req = this._dequeue(m.tid)
|
|
|
|
if (req === null) return
|
|
|
|
// decrement the inflight window as this is an "ack"
|
|
if (this._win[this._w] > 0) this._win[this._w]--
|
|
this.onresponse(m, this)
|
|
|
|
if (m.status === 0 || req.expectOk === false) {
|
|
req.resolve(m)
|
|
} else {
|
|
req.reject(createStatusError(m.status))
|
|
}
|
|
}
|
|
|
|
_send (socket, buf, addr) {
|
|
if (this.destroyed) return
|
|
socket.send(buf, 0, buf.byteLength, addr.port, addr.host)
|
|
}
|
|
|
|
_dequeue (tid) {
|
|
for (let i = 0; i < this.inflight.length; i++) {
|
|
const req = this.inflight[i]
|
|
|
|
if (req.tid === tid) {
|
|
if (i === this.inflight.length - 1) this.inflight.pop()
|
|
else this.inflight[i] = this.inflight.pop()
|
|
return req
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
_drain () {
|
|
let total = this._win[0] + this._win[1] + this._win[2] + this._win[3]
|
|
|
|
for (let i = 0; i < this.inflight.length; i++) {
|
|
const req = this.inflight[i]
|
|
|
|
if (req.tries > 0 && --req.timeout >= 0) continue
|
|
req.timeout = 2
|
|
|
|
if (req.tries++ > this.maxRetries) {
|
|
if (i === this.inflight.length - 1) this.inflight.pop()
|
|
else this.inflight[i] = this.inflight.pop()
|
|
req.reject(createTimeoutError())
|
|
continue
|
|
}
|
|
|
|
if (total >= 2 * this.maxWindow || this._win[this._w] >= this.maxWindow) {
|
|
req.tries--
|
|
continue
|
|
}
|
|
|
|
total++
|
|
this._win[this._w]++
|
|
this._send(req.socket, req.buffer, req.to)
|
|
}
|
|
|
|
this._w = (this._w + 1) & 3
|
|
this._win[this._w] = 0 // clear oldest
|
|
}
|
|
}
|
|
|
|
function createTimeoutError () {
|
|
const err = new Error('Request timed out')
|
|
err.status = 0
|
|
return err
|
|
}
|
|
|
|
function createStatusError (status) {
|
|
const err = new Error('Request failed with status ' + status)
|
|
err.status = status
|
|
return err
|
|
}
|
|
|
|
function noop () {}
|
|
|