feat: ping time internet quality measurement

This commit is contained in:
José Oliveira 2021-08-30 18:40:12 +01:00 committed by Josh Harvey
parent f2f146265e
commit 515d02dd21
7 changed files with 162 additions and 17 deletions

View file

@ -1,5 +1,7 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const axios = require('axios') const axios = require('axios')
const uuid = require('uuid')
const db = require('./db') const db = require('./db')
const pairing = require('./pairing') const pairing = require('./pairing')
@ -56,19 +58,21 @@ function addName (pings, events, config) {
getStatus( getStatus(
_.first(pings[machine.deviceId]), _.first(pings[machine.deviceId]),
_.first(checkStuckScreen(events, machine.name)) _.first(checkStuckScreen(events, machine.name))
) )
] ]
return _.assign(machine, { cashOut, statuses }) return _.assign(machine, { cashOut, statuses })
} }
} }
function getMachineNames (config) { function getMachineNames (config) {
return Promise.all([getMachines(), getConfig(config)]) return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
.then(([machines, config]) => Promise.all( .then(([rawMachines, config, heartbeat, performance]) => Promise.all(
[machines, checkPings(machines), dbm.machineEvents(), config] [rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
)) ))
.then(([machines, pings, events, config]) => { .then(([rawMachines, pings, events, config, heartbeat, performance]) => {
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
return machines.map(addName(pings, events, config)) return machines.map(addName(pings, events, config))
}) })
} }
@ -105,8 +109,8 @@ function getMachine (machineId, config) {
})) }))
return Promise.all([queryMachine, dbm.machineEvents(), config]) return Promise.all([queryMachine, dbm.machineEvents(), config])
.then(([machine, events, config]) => { .then(([machine, events, config]) => {
const pings = checkPings([machine]) const pings = checkPings([machine])
return [machine].map(addName(pings, events, config))[0] return [machine].map(addName(pings, events, config))[0]
}) })
@ -163,4 +167,56 @@ function setMachine (rec) {
} }
} }
module.exports = { getMachineName, getMachines, getMachine, getMachineNames, setMachine } function updateNetworkPerformance (deviceId, data) {
const downloadSpeed = _.head(data)
const dbData = {
device_id: deviceId,
download_speed: downloadSpeed.speed,
created: new Date()
}
const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
{ table: 'machine_network_performance' })
const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
const upsert = pgp.helpers.insert(dbData, cs) + onConflict
return db.none(upsert)
}
function updateNetworkHeartbeat (deviceId, data) {
const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
const dbData = {
id: uuid.v4(),
device_id: deviceId,
average_response_time: avgResponseTime,
average_packet_loss: avgPacketLoss
}
const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
return db.none(sql)
}
function getNetworkPerformance () {
const sql = `SELECT device_id, download_speed FROM machine_network_performance`
return db.manyOrNone(sql)
.then(res => _.map(_.mapKeys(_.camelCase))(res))
}
function getNetworkHeartbeat () {
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
FROM machine_network_heartbeat
GROUP BY device_id`
return db.manyOrNone(sql)
.then(res => _.map(_.mapKeys(_.camelCase))(res))
}
module.exports = {
getMachineName,
getMachines,
getMachine,
getMachineNames,
setMachine,
updateNetworkPerformance,
updateNetworkHeartbeat,
getNetworkPerformance,
getNetworkHeartbeat
}

View file

@ -79,6 +79,9 @@ const typeDefs = gql`
cassette2: Int cassette2: Int
statuses: [MachineStatus] statuses: [MachineStatus]
latestEvent: MachineEvent latestEvent: MachineEvent
downloadSpeed: String
responseTime: String
packetLoss: String
} }
type Customer { type Customer {

View file

@ -276,6 +276,13 @@ function plugins (settings, deviceId) {
]) ])
} }
function pruneMachinesHeartbeat () {
const sql = `DELETE FROM machine_network_heartbeat h
USING (SELECT device_id, max(created) as lastEntry FROM machine_network_heartbeat GROUP BY device_id) d
WHERE d.device_id = h.device_id AND h.created < d.lastEntry`
db.none(sql)
}
function isHd (tx) { function isHd (tx) {
return wallet.isHd(settings, tx.cryptoCode) return wallet.isHd(settings, tx.cryptoCode)
} }
@ -743,7 +750,8 @@ function plugins (settings, deviceId) {
sell, sell,
getNotificationConfig, getNotificationConfig,
notifyOperator, notifyOperator,
fetchCurrentConfigVersion fetchCurrentConfigVersion,
pruneMachinesHeartbeat
} }
} }

View file

@ -24,6 +24,7 @@ const LOGS_CLEAR_INTERVAL = 1 * T.day
const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes
const SANCTIONS_UPDATE_INTERVAL = 1 * T.week const SANCTIONS_UPDATE_INTERVAL = 1 * T.week
const RADAR_UPDATE_INTERVAL = 5 * T.minutes const RADAR_UPDATE_INTERVAL = 5 * T.minutes
const PRUNE_MACHINES_HEARBEAT = 1 * T.day
const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds
@ -102,6 +103,7 @@ function start (__settings) {
setInterval(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL) setInterval(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL)
setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL) setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL)
setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL) setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL)
setInterval(() => pi().pruneMachinesHeartbeat(), PRUNE_MACHINES_HEARBEAT)
} }
module.exports = { start, reload } module.exports = { start, reload }

View file

@ -14,6 +14,7 @@ const dbErrorCodes = require('./db-error-codes')
const options = require('./options') const options = require('./options')
const logger = require('./logger') const logger = require('./logger')
const configManager = require('./new-config-manager') const configManager = require('./new-config-manager')
const machineLoader = require('./machine-loader')
const complianceTriggers = require('./compliance-triggers') const complianceTriggers = require('./compliance-triggers')
const pairing = require('./pairing') const pairing = require('./pairing')
const newSettingsLoader = require('./new-settings-loader') const newSettingsLoader = require('./new-settings-loader')
@ -243,6 +244,18 @@ function verifyPromoCode (req, res, next) {
.catch(next) .catch(next)
} }
function networkHeartbeat (req, res, next) {
return machineLoader.updateNetworkHeartbeat(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
function networkPerformance (req, res, next) {
return machineLoader.updateNetworkPerformance(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
function addOrUpdateCustomer (req) { function addOrUpdateCustomer (req) {
const customerData = req.body const customerData = req.body
const machineVersion = req.query.version const machineVersion = req.query.version
@ -516,6 +529,9 @@ app.get('/poll', poll)
app.get('/terms_conditions', getTermsConditions) app.get('/terms_conditions', getTermsConditions)
app.post('/state', stateChange) app.post('/state', stateChange)
app.post('/network/heartbeat', networkHeartbeat)
app.post('/network/performance', networkPerformance)
app.post('/verify_user', verifyUser) app.post('/verify_user', verifyUser)
app.post('/verify_transaction', verifyTx) app.post('/verify_transaction', verifyTx)
app.post('/verify_promo_code', verifyPromoCode) app.post('/verify_promo_code', verifyPromoCode)

View file

@ -0,0 +1,26 @@
const db = require('./db')
exports.up = function (next) {
var sql = [
'DROP TABLE IF EXISTS machine_network_heartbeat',
'DROP TABLE IF EXISTS machine_network_performance',
`CREATE TABLE machine_network_performance (
device_id text PRIMARY KEY,
download_speed numeric NOT NULL,
created timestamptz NOT NULL default now()
)`,
`CREATE TABLE machine_network_heartbeat (
id uuid PRIMARY KEY,
device_id text not null,
average_response_time numeric NOT NULL,
average_packet_loss numeric NOT NULL,
created timestamptz NOT NULL default now()
)`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -1,5 +1,6 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import moment from 'moment' import moment from 'moment'
import * as R from 'ramda' import * as R from 'ramda'
@ -33,6 +34,9 @@ const GET_MACHINES = gql`
label label
type type
} }
downloadSpeed
responseTime
packetLoss
} }
} }
` `
@ -48,25 +52,55 @@ const MachineStatus = () => {
const elements = [ const elements = [
{ {
header: 'Machine Name', header: 'Machine Name',
width: 250, width: 150,
size: 'sm', size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => m.name view: m => m.name
}, },
{ {
header: 'Status', header: 'Status',
width: 350, width: 150,
size: 'sm', size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => <MainStatus statuses={m.statuses} /> view: m => <MainStatus statuses={m.statuses} />
}, },
{ {
header: 'Last ping', header: 'Last ping',
width: 200, width: 175,
size: 'sm', size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown') view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown')
}, },
{
header: 'Network speed',
width: 150,
size: 'sm',
textAlign: 'left',
view: m =>
m.downloadSpeed
? new BigNumber(m.downloadSpeed).toFixed(4).toString() + ' MB/s'
: 'unavailable'
},
{
header: 'Latency',
width: 150,
size: 'sm',
textAlign: 'left',
view: m =>
m.responseTime
? new BigNumber(m.responseTime).toFixed(3).toString() + ' ms'
: 'unavailable'
},
{
header: 'Packet Loss',
width: 125,
size: 'sm',
textAlign: 'left',
view: m =>
m.packetLoss
? new BigNumber(m.packetLoss).toFixed(3).toString() + ' %'
: 'unavailable'
},
{ {
header: 'Software Version', header: 'Software Version',
width: 200, width: 200,