From 515d02dd217970c1915591672238127a3deeea8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Mon, 30 Aug 2021 18:40:12 +0100 Subject: [PATCH] feat: ping time internet quality measurement --- lib/machine-loader.js | 82 ++++++++++++++++--- lib/new-admin/graphql/schema.js | 3 + lib/plugins.js | 10 ++- lib/poller.js | 2 + lib/routes.js | 16 ++++ ...5844773-add-machine-network-performance.js | 26 ++++++ .../src/pages/Maintenance/MachineStatus.js | 40 ++++++++- 7 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 migrations/1626275844773-add-machine-network-performance.js diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 2caf99b0..a51aa8ac 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -1,5 +1,7 @@ const _ = require('lodash/fp') +const pgp = require('pg-promise')() const axios = require('axios') +const uuid = require('uuid') const db = require('./db') const pairing = require('./pairing') @@ -49,26 +51,28 @@ const getStatus = (ping, stuck) => { function addName (pings, events, config) { return machine => { const cashOutConfig = configManager.getCashOut(machine.deviceId, config) - + const cashOut = !!cashOutConfig.active - + const statuses = [ getStatus( _.first(pings[machine.deviceId]), _.first(checkStuckScreen(events, machine.name)) - ) - ] - + ) + ] + return _.assign(machine, { cashOut, statuses }) } } function getMachineNames (config) { - return Promise.all([getMachines(), getConfig(config)]) - .then(([machines, config]) => Promise.all( - [machines, checkPings(machines), dbm.machineEvents(), config] + return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()]) + .then(([rawMachines, config, heartbeat, performance]) => Promise.all( + [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)) }) } @@ -105,9 +109,9 @@ function getMachine (machineId, config) { })) return Promise.all([queryMachine, dbm.machineEvents(), config]) - .then(([machine, events, config]) => { - const pings = checkPings([machine]) - + .then(([machine, events, config]) => { + const pings = checkPings([machine]) + 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 +} diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 57d7d66a..c57bae2f 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -79,6 +79,9 @@ const typeDefs = gql` cassette2: Int statuses: [MachineStatus] latestEvent: MachineEvent + downloadSpeed: String + responseTime: String + packetLoss: String } type Customer { diff --git a/lib/plugins.js b/lib/plugins.js index 6406dda8..0cca0bd4 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -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) { return wallet.isHd(settings, tx.cryptoCode) } @@ -743,7 +750,8 @@ function plugins (settings, deviceId) { sell, getNotificationConfig, notifyOperator, - fetchCurrentConfigVersion + fetchCurrentConfigVersion, + pruneMachinesHeartbeat } } diff --git a/lib/poller.js b/lib/poller.js index e03bd689..52d43f76 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -24,6 +24,7 @@ const LOGS_CLEAR_INTERVAL = 1 * T.day const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes const SANCTIONS_UPDATE_INTERVAL = 1 * T.week const RADAR_UPDATE_INTERVAL = 5 * T.minutes +const PRUNE_MACHINES_HEARBEAT = 1 * T.day const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds @@ -102,6 +103,7 @@ function start (__settings) { setInterval(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL) setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL) setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL) + setInterval(() => pi().pruneMachinesHeartbeat(), PRUNE_MACHINES_HEARBEAT) } module.exports = { start, reload } diff --git a/lib/routes.js b/lib/routes.js index 965f06d8..baee01ea 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -14,6 +14,7 @@ const dbErrorCodes = require('./db-error-codes') const options = require('./options') const logger = require('./logger') const configManager = require('./new-config-manager') +const machineLoader = require('./machine-loader') const complianceTriggers = require('./compliance-triggers') const pairing = require('./pairing') const newSettingsLoader = require('./new-settings-loader') @@ -243,6 +244,18 @@ function verifyPromoCode (req, res, 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) { const customerData = req.body const machineVersion = req.query.version @@ -516,6 +529,9 @@ app.get('/poll', poll) app.get('/terms_conditions', getTermsConditions) app.post('/state', stateChange) +app.post('/network/heartbeat', networkHeartbeat) +app.post('/network/performance', networkPerformance) + app.post('/verify_user', verifyUser) app.post('/verify_transaction', verifyTx) app.post('/verify_promo_code', verifyPromoCode) diff --git a/migrations/1626275844773-add-machine-network-performance.js b/migrations/1626275844773-add-machine-network-performance.js new file mode 100644 index 00000000..ee367998 --- /dev/null +++ b/migrations/1626275844773-add-machine-network-performance.js @@ -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() +} \ No newline at end of file diff --git a/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js b/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js index e2f5fe4b..8423696c 100644 --- a/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js @@ -1,5 +1,6 @@ import { useQuery } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core' +import BigNumber from 'bignumber.js' import gql from 'graphql-tag' import moment from 'moment' import * as R from 'ramda' @@ -33,6 +34,9 @@ const GET_MACHINES = gql` label type } + downloadSpeed + responseTime + packetLoss } } ` @@ -48,25 +52,55 @@ const MachineStatus = () => { const elements = [ { header: 'Machine Name', - width: 250, + width: 150, size: 'sm', textAlign: 'left', view: m => m.name }, { header: 'Status', - width: 350, + width: 150, size: 'sm', textAlign: 'left', view: m => }, { header: 'Last ping', - width: 200, + width: 175, size: 'sm', textAlign: 'left', 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', width: 200,