From 030c6ec3f620c9694e0afc931573b372b66808fd Mon Sep 17 00:00:00 2001 From: Yuki von Kanel Date: Thu, 30 Apr 2020 15:13:08 -0500 Subject: [PATCH] Fix using stale Redis stats, parse info directly The ioredis client used by bull makes no guarantees that the `serverInfo` field be updated upon issuing an INFO command, and in fact does not appear to do so (given bull 3.13.0 and ioredis 4.16.3). While the `serverInfo` field is indeed populated after the initial connection is established, it is not updated by calling INFO as assumed with the previous logic. This results in stale data being presented by Arena. Rather than relying on this undocumented and evidently fragile behaviour, we can instead parse the result of the INFO command on our own. This also happens to step around an issue with the way that ioredis parses key-value data in handling the INFO response, where values containing the colon character are incorrectly trimmed. Given the stability and superb backwards-compatibility of Redis, this should be a highly reliable solution. --- src/server/views/helpers/queueHelpers.js | 42 ++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/server/views/helpers/queueHelpers.js b/src/server/views/helpers/queueHelpers.js index 75a36a85..35f2c0c3 100644 --- a/src/server/views/helpers/queueHelpers.js +++ b/src/server/views/helpers/queueHelpers.js @@ -28,11 +28,49 @@ function formatBytes(num) { return (neg ? '-' : '') + numStr + ' ' + unit; } +/** + * Parses a Redis server info string as returned by `INFO`, generating an + * output object populated with fields based upon key-value pairs read. + * + * @param {string} rawServerInfo - Redis server info as returned from the + * `INFO` command. + * + * @returns {object} Returns a dictionary with key-value pairs based upon + * the parsed server info. + */ +function parseRedisServerInfo(rawServerInfo) { + // Return just an empty object if we've not been given a proper string. + if (typeof rawServerInfo !== 'string') { + return {}; + } + + const out = {}; + const lines = rawServerInfo.split('\r\n'); + for (const line of lines) { + // Skip section names and empty lines. + if (line.startsWith('#') || !line.length) { + continue; + } + // Split our line into key:value components. Note that it is possible + // for the value to contain the colon character, so we must take care + // to have all potential components joined rather than assuming there + // is only one. + const [key, ...valueComponents] = line.split(':'); + const value = valueComponents.join(':'); + if (value) { + out[key] = value; + } + } + + return out; +} + const Helpers = { getStats: async function(queue) { - await queue.client.info(); // update queue.client.serverInfo + const rawServerInfo = await queue.client.info(); + const serverInfo = parseRedisServerInfo(rawServerInfo); - const stats = _.pickBy(queue.client.serverInfo, (value, key) => _.includes(this._usefulMetrics, key)); + const stats = _.pickBy(serverInfo, (value, key) => _.includes(this._usefulMetrics, key)); stats.used_memory = formatBytes(parseInt(stats.used_memory, 10)); stats.total_system_memory = formatBytes(parseInt(stats.total_system_memory, 10)); return stats;