From 6a9c62b6398d065f1268bfbb716e660fe626296a Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Fri, 10 Nov 2017 14:34:24 -0800 Subject: [PATCH] Support time discretization in scripts This can be used to ensure parameter refresh times are aligned (as opposed to using Date.now() - X). It also allows for possible future optimizations by exploiting the fact that the value of Date.now(X) remains static for a given amount of time. The variance value can be used to shift the discretization threshold points based on the hash of the device ID for load distribution. The djb2 hashing algorithm has been replaced with MD5 for better distribution. --- lib/common.coffee | 10 ----- lib/cwmp.coffee | 33 +++++------------ lib/default-provisions.js | 5 ++- lib/local-cache.js | 9 +---- lib/sandbox.js | 33 +++++++++++++++-- lib/scheduling.js | 78 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 lib/scheduling.js diff --git a/lib/common.coffee b/lib/common.coffee index 7edf83c..87e953f 100644 --- a/lib/common.coffee +++ b/lib/common.coffee @@ -18,7 +18,6 @@ ### buffer = require 'buffer' -querystring = require 'querystring' UNDEFINED_TYPE = '[object Undefined]' NULL_TYPE = '[object Null]' @@ -224,14 +223,6 @@ escapeRegExp = (str) -> return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") -# djb2 hashing algorithm adapted from http://www.cse.yorku.ca/~oz/hash.html -djb2 = (str) -> - hash = 5381 - for i in [0...str.length] - hash = ((hash << 5) + hash) ^ str.charCodeAt(i) - return hash - - exports.UNDEFINED_TYPE = UNDEFINED_TYPE exports.NULL_TYPE = NULL_TYPE exports.NUMBER_TYPE = NUMBER_TYPE @@ -246,4 +237,3 @@ exports.parsePath = parsePath exports.addPathMeta = addPathMeta exports.hammingWeight = hammingWeight exports.escapeRegExp = escapeRegExp -exports.djb2 = djb2 diff --git a/lib/cwmp.coffee b/lib/cwmp.coffee index 09457b3..54411c4 100644 --- a/lib/cwmp.coffee +++ b/lib/cwmp.coffee @@ -31,6 +31,8 @@ cache = require './cache' localCache = require './local-cache' db = require './db' logger = require './logger' +scheduling = require './scheduling' + MAX_CYCLES = 4 @@ -198,28 +200,6 @@ transferComplete = (sessionContext, rpc) -> ) -testSchedule = (schedule, timestamp) -> - range = schedule.schedule.nextRange(10, new Date(timestamp)) - prev = schedule.schedule.prevRange(1, new Date(timestamp), new Date(timestamp - schedule.duration)) - - if prev - first = +(prev[0] ? 0) - last = +(prev[1] ? 0) + schedule.duration - else - first = +(range[0][0] ? 0) - last = +(range[0][1] ? 0) + schedule.duration - - for r in range - if last < r[0] - break - last = +(r[1] ? 0) + schedule.duration - - if timestamp >= first - return [true, Math.max(0, last - timestamp)] - else - return [false, first - timestamp] - - # Append providions and remove duplicates appendProvisions = (original, toAppend) -> modified = false @@ -293,8 +273,13 @@ applyPresets = (sessionContext) -> eventsMatch = false break - continue if (not eventsMatch) or (preset.schedule and not - (preset.schedule.schedule and testSchedule(preset.schedule, sessionContext.timestamp)[0])) + continue if not eventsMatch + + if preset.schedule + continue if not preset.schedule.schedule + r = scheduling.cron(sessionContext.timestamp, preset.schedule.schedule) + if not (r[0] + preset.schedule.duration > sessionContext.timestamp) + continue filteredPresets.push(preset) for k of preset.precondition diff --git a/lib/default-provisions.js b/lib/default-provisions.js index ff63625..1559380 100644 --- a/lib/default-provisions.js +++ b/lib/default-provisions.js @@ -21,6 +21,7 @@ const config = require("./config"); const common = require("./common"); const device = require("./device"); +const scheduling = require("./scheduling"); const MAX_DEPTH = config.get("MAX_DEPTH"); @@ -31,8 +32,8 @@ function refresh(sessionContext, provision, declarations, startRevision, endRevi path.length = MAX_DEPTH; path.fill("*", l); let every = 1000 * (provision[2] || 1); - let offset = Math.abs(common.djb2(sessionContext.deviceId)) % 86400000; - let t = Math.trunc((sessionContext.timestamp + offset) / every) * every - offset; + let offset = scheduling.variance(sessionContext.deviceId, every); + let t = scheduling.interval(sessionContext.timestamp, every, offset); for (let i = l; i < path.length; ++ i) declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]); diff --git a/lib/local-cache.js b/lib/local-cache.js index b5d1b9b..1992a98 100644 --- a/lib/local-cache.js +++ b/lib/local-cache.js @@ -21,12 +21,11 @@ const vm = require("vm"); const crypto = require("crypto"); -const later = require("later"); - const db = require("./db"); const cache = require("./cache"); const query = require("./query"); const logger = require("./logger"); +const scheduling = require("./scheduling"); const REFRESH = 3000; @@ -125,11 +124,7 @@ function refresh(callback) { try { schedule.duration = +(parts.shift()) * 1000; - if (parts.length === 5) { - parts.unshift("0"); - // TODO later.js doesn't throw erorr if expression is invalid! - schedule.schedule = later.schedule(later.parse.cron(parts.join(" "), true)); - } + schedule.schedule = scheduling.parseCron(parts.join(" ")); } catch (err) { logger.warn({ diff --git a/lib/sandbox.js b/lib/sandbox.js index 571feee..d661c49 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -26,6 +26,7 @@ const common = require("./common"); const device = require("./device"); const extensions = require("./extensions"); const logger = require("./logger"); +const scheduling = require("./scheduling"); // Used for throwing to exit user script and commit @@ -40,15 +41,39 @@ let state; class SandboxDate { - constructor(arg) { + constructor() { if (arguments.length) - return new (Function.prototype.bind.apply(Date, arguments)); + return new Date(...arguments); return new Date(state.sessionContext.timestamp); } - static now() { - return state.sessionContext.timestamp; + static now(intervalOrCron, variance) { + let t = state.sessionContext.timestamp; + + if (typeof intervalOrCron === "number") { + + if (variance == null) + variance = intervalOrCron; + + let offset = 0; + if (variance) + offset = scheduling.variance(state.sessionContext.deviceId, variance); + + t = scheduling.interval(t, intervalOrCron, offset); + } + else if (typeof intervalOrCron === "string") { + let offset = 0; + if (variance) + offset = scheduling.variance(state.sessionContext.deviceId, variance); + let cron = scheduling.parseCron(intervalOrCron); + t = scheduling.cron(t, cron, offset)[0]; + } + else if (intervalOrCron) { + throw new Error("Invalid Date.now() argument"); + } + + return t; } }; diff --git a/lib/scheduling.js b/lib/scheduling.js new file mode 100644 index 0000000..2a4c8aa --- /dev/null +++ b/lib/scheduling.js @@ -0,0 +1,78 @@ +/** + * Copyright 2013-2017 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ +"use strict"; + +const crypto = require("crypto"); + +const later = require("later"); + + +function md532(str) { + const digest = crypto + .createHash("md5") + .update(str) + .digest(); + return ( + digest.readUInt32LE(0) ^ + digest.readUInt32LE(4) ^ + digest.readUInt32LE(8) ^ + digest.readUInt32LE(12) + ); +} + + +function variance(deviceId, variance) { + return (md532(deviceId) >>> 0) % variance; +} + + +function interval(timestamp, interval, offset = 0) { + return Math.trunc((timestamp + offset) / interval) * interval - offset; +} + + +function parseCron(cronExp) { + let parts = cronExp.trim().split(/\s+/); + if (parts.length === 5) + parts.unshift("*"); + + return later.schedule(later.parse.cron(parts.join(" "), true)); +} + + +function cron(timestamp, schedule, offset = 0) { + // TODO later.js doesn't throw erorr if expression is invalid! + const ret = [0, 0]; + + let prev = schedule.prev(1, new Date(timestamp + offset)); + if (prev) + ret[0] = prev.setMilliseconds(0) - offset; + + let next = schedule.next(1, new Date(timestamp + offset + 1000)); + if (next) + ret[1] = next.setMilliseconds(0) - offset; + + return ret; +} + + +exports.variance = variance; +exports.interval = interval; +exports.parseCron = parseCron; +exports.cron = cron;