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.
This commit is contained in:
Zaid Abdulla committed 2017-11-10 14:34:24 -08:00
1 parent ed873cdd4a
commit 6a9c62b639
6 files changed
+121 -47

No files matched your search

-10
View File
@@ -18,7 +18,6 @@
### ###
buffer = require 'buffer' buffer = require 'buffer'
querystring = require 'querystring'
UNDEFINED_TYPE = '[object Undefined]' UNDEFINED_TYPE = '[object Undefined]'
NULL_TYPE = '[object Null]' NULL_TYPE = '[object Null]'
@@ -224,14 +223,6 @@ escapeRegExp = (str) ->
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") 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.UNDEFINED_TYPE = UNDEFINED_TYPE
exports.NULL_TYPE = NULL_TYPE exports.NULL_TYPE = NULL_TYPE
exports.NUMBER_TYPE = NUMBER_TYPE exports.NUMBER_TYPE = NUMBER_TYPE
@@ -246,4 +237,3 @@ exports.parsePath = parsePath
exports.addPathMeta = addPathMeta exports.addPathMeta = addPathMeta
exports.hammingWeight = hammingWeight exports.hammingWeight = hammingWeight
exports.escapeRegExp = escapeRegExp exports.escapeRegExp = escapeRegExp
exports.djb2 = djb2
+9 -24
View File
@@ -31,6 +31,8 @@ cache = require './cache'
localCache = require './local-cache' localCache = require './local-cache'
db = require './db' db = require './db'
logger = require './logger' logger = require './logger'
scheduling = require './scheduling'
MAX_CYCLES = 4 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 # Append providions and remove duplicates
appendProvisions = (original, toAppend) -> appendProvisions = (original, toAppend) ->
modified = false modified = false
@@ -293,8 +273,13 @@ applyPresets = (sessionContext) ->
eventsMatch = false eventsMatch = false
break break
continue if (not eventsMatch) or (preset.schedule and not continue if not eventsMatch
(preset.schedule.schedule and testSchedule(preset.schedule, sessionContext.timestamp)[0]))
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) filteredPresets.push(preset)
for k of preset.precondition for k of preset.precondition
+3 -2
View File
@@ -21,6 +21,7 @@
const config = require("./config"); const config = require("./config");
const common = require("./common"); const common = require("./common");
const device = require("./device"); const device = require("./device");
const scheduling = require("./scheduling");
const MAX_DEPTH = config.get("MAX_DEPTH"); const MAX_DEPTH = config.get("MAX_DEPTH");
@@ -31,8 +32,8 @@ function refresh(sessionContext, provision, declarations, startRevision, endRevi
path.length = MAX_DEPTH; path.length = MAX_DEPTH;
path.fill("*", l); path.fill("*", l);
let every = 1000 * (provision[2] || 1); let every = 1000 * (provision[2] || 1);
let offset = Math.abs(common.djb2(sessionContext.deviceId)) % 86400000; let offset = scheduling.variance(sessionContext.deviceId, every);
let t = Math.trunc((sessionContext.timestamp + offset) / every) * every - offset; let t = scheduling.interval(sessionContext.timestamp, every, offset);
for (let i = l; i < path.length; ++ i) for (let i = l; i < path.length; ++ i)
declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]); declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]);
+2 -7
View File
@@ -21,12 +21,11 @@
const vm = require("vm"); const vm = require("vm");
const crypto = require("crypto"); const crypto = require("crypto");
const later = require("later");
const db = require("./db"); const db = require("./db");
const cache = require("./cache"); const cache = require("./cache");
const query = require("./query"); const query = require("./query");
const logger = require("./logger"); const logger = require("./logger");
const scheduling = require("./scheduling");
const REFRESH = 3000; const REFRESH = 3000;
@@ -125,11 +124,7 @@ function refresh(callback) {
try { try {
schedule.duration = +(parts.shift()) * 1000; schedule.duration = +(parts.shift()) * 1000;
if (parts.length === 5) { schedule.schedule = scheduling.parseCron(parts.join(" "));
parts.unshift("0");
// TODO later.js doesn't throw erorr if expression is invalid!
schedule.schedule = later.schedule(later.parse.cron(parts.join(" "), true));
}
} }
catch (err) { catch (err) {
logger.warn({ logger.warn({
+29 -4
View File
@@ -26,6 +26,7 @@ const common = require("./common");
const device = require("./device"); const device = require("./device");
const extensions = require("./extensions"); const extensions = require("./extensions");
const logger = require("./logger"); const logger = require("./logger");
const scheduling = require("./scheduling");
// Used for throwing to exit user script and commit // Used for throwing to exit user script and commit
@@ -40,15 +41,39 @@ let state;
class SandboxDate { class SandboxDate {
constructor(arg) { constructor() {
if (arguments.length) if (arguments.length)
return new (Function.prototype.bind.apply(Date, arguments)); return new Date(...arguments);
return new Date(state.sessionContext.timestamp); return new Date(state.sessionContext.timestamp);
} }
static now() { static now(intervalOrCron, variance) {
return state.sessionContext.timestamp; 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;
} }
}; };
+78
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
"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;