implement and test a complex constrained scheduler
parent
47fdf9de9f
commit
7072fe4fa4
@ -0,0 +1,173 @@
|
||||
var WriteQueue = require("./write-queue");
|
||||
var Util = require("./common-util");
|
||||
|
||||
/* This module provides implements a FIFO scheduler
|
||||
which assumes the existence of three types of async tasks:
|
||||
|
||||
1. ordered tasks which must be executed sequentially
|
||||
2. unordered tasks which can be executed in parallel
|
||||
3. blocking tasks which must block the execution of all other tasks
|
||||
|
||||
The scheduler assumes there will be many resources identified by strings,
|
||||
and that the constraints described above will only apply in the context
|
||||
of identical string ids.
|
||||
|
||||
Many blocking tasks may be executed in parallel so long as they
|
||||
concern resources identified by different ids.
|
||||
|
||||
USAGE:
|
||||
|
||||
const schedule = require("./schedule")();
|
||||
|
||||
// schedule two sequential tasks using the resource 'pewpew'
|
||||
schedule.ordered('pewpew', function (next) {
|
||||
appendToFile('beep\n', next);
|
||||
});
|
||||
schedule.ordered('pewpew', function (next) {
|
||||
appendToFile('boop\n', next);
|
||||
});
|
||||
|
||||
// schedule a task that can happen whenever
|
||||
schedule.unordered('pewpew', function (next) {
|
||||
displayFileSize(next);
|
||||
});
|
||||
|
||||
// schedule a blocking task which will wait
|
||||
// until the all unordered tasks have completed before commencing
|
||||
schedule.blocking('pewpew', function (next) {
|
||||
deleteFile(next);
|
||||
});
|
||||
|
||||
// this will be queued for after the blocking task
|
||||
schedule.ordered('pewpew', function (next) {
|
||||
appendFile('boom', next);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
// return a uid which is not already in a map
|
||||
var unusedUid = function (set) {
|
||||
var uid = Util.uid();
|
||||
if (set[uid]) { return unusedUid(); }
|
||||
return uid;
|
||||
};
|
||||
|
||||
// return an existing session, creating one if it does not already exist
|
||||
var lookup = function (map, id) {
|
||||
return (map[id] = map[id] || {
|
||||
//blocking: [],
|
||||
active: {},
|
||||
blocked: {},
|
||||
});
|
||||
};
|
||||
|
||||
var isEmpty = function (map) {
|
||||
for (var key in map) {
|
||||
if (map.hasOwnProperty(key)) { return false; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// XXX enforce asynchrony everywhere
|
||||
module.exports = function () {
|
||||
// every scheduler instance has its own queue
|
||||
var queue = WriteQueue();
|
||||
|
||||
// ordered tasks don't require any extra logic
|
||||
var Ordered = function (id, task) {
|
||||
queue(id, task);
|
||||
};
|
||||
|
||||
// unordered and blocking tasks need a little extra state
|
||||
var map = {};
|
||||
|
||||
// regular garbage collection keeps memory consumption low
|
||||
var collectGarbage = function (id) {
|
||||
// avoid using 'lookup' since it creates a session implicitly
|
||||
var local = map[id];
|
||||
// bail out if no session
|
||||
if (!local) { return; }
|
||||
// bail out if there are blocking or active tasks
|
||||
if (local.lock) { return; }
|
||||
if (!isEmpty(local.active)) { return; }
|
||||
// if there are no pending actions then delete the session
|
||||
delete map[id];
|
||||
};
|
||||
|
||||
// unordered tasks run immediately if there are no blocking tasks scheduled
|
||||
// or immediately after blocking tasks finish
|
||||
var runImmediately = function (local, task) {
|
||||
// set a flag in the map of active unordered tasks
|
||||
// to prevent blocking tasks from running until you finish
|
||||
var uid = unusedUid(local.active);
|
||||
local.active[uid] = true;
|
||||
|
||||
task(function () {
|
||||
// remove the flag you set to indicate that your task completed
|
||||
delete local.active[uid];
|
||||
// don't do anything if other unordered tasks are still running
|
||||
if (!isEmpty(local.active)) { return; }
|
||||
// bail out if there are no blocking tasks scheduled or ready
|
||||
if (typeof(local.waiting) !== 'function') {
|
||||
return void collectGarbage();
|
||||
}
|
||||
local.waiting();
|
||||
});
|
||||
};
|
||||
|
||||
var runOnceUnblocked = function (local, task) {
|
||||
var uid = unusedUid(local.blocked);
|
||||
local.blocked[uid] = function () {
|
||||
runImmediately(local, task);
|
||||
};
|
||||
};
|
||||
|
||||
// 'unordered' tasks are scheduled to run in after the most recently received blocking task
|
||||
// or immediately and in parallel if there are no blocking tasks scheduled.
|
||||
var Unordered = function (id, task) {
|
||||
var local = lookup(map, id);
|
||||
if (local.lock) { return runOnceUnblocked(local, task); }
|
||||
runImmediately(local, task);
|
||||
};
|
||||
|
||||
var runBlocked = function (local) {
|
||||
for (var task in local.blocked) {
|
||||
runImmediately(local, local.blocked[task]);
|
||||
}
|
||||
};
|
||||
|
||||
// 'blocking' tasks must be run alone.
|
||||
// They are queued alongside ordered tasks,
|
||||
// and wait until any running 'unordered' tasks complete before commencing.
|
||||
var Blocking = function (id, task) {
|
||||
var local = lookup(map, id);
|
||||
|
||||
queue(id, function (next) {
|
||||
// start right away if there are no running unordered tasks
|
||||
if (isEmpty(local.active)) {
|
||||
local.lock = true;
|
||||
return void task(function () {
|
||||
delete local.lock;
|
||||
runBlocked(local);
|
||||
next();
|
||||
});
|
||||
}
|
||||
// otherwise wait until the running tasks have completed
|
||||
local.waiting = function () {
|
||||
local.lock = true;
|
||||
task(function () {
|
||||
delete local.lock;
|
||||
delete local.waiting;
|
||||
runBlocked(local);
|
||||
next();
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
ordered: Ordered,
|
||||
unordered: Unordered,
|
||||
blocking: Blocking,
|
||||
};
|
||||
};
|
@ -0,0 +1,220 @@
|
||||
/* three types of actions:
|
||||
* read
|
||||
* write
|
||||
* append
|
||||
each of which take a random amount of time
|
||||
|
||||
*/
|
||||
var Util = require("../../lib/common-util");
|
||||
var schedule = require("../../lib/schedule")();
|
||||
var nThen = require("nthen");
|
||||
|
||||
var rand = function (n) {
|
||||
return Math.floor(Math.random() * n);
|
||||
};
|
||||
|
||||
var rand_time = function () {
|
||||
// between 51 and 151
|
||||
return rand(300) + 25;
|
||||
};
|
||||
|
||||
var makeAction = function (type) {
|
||||
var i = 0;
|
||||
return function (time) {
|
||||
var j = i++;
|
||||
return function (next) {
|
||||
console.log(" Beginning action: %s#%s", type, j);
|
||||
setTimeout(function () {
|
||||
console.log(" Completed action: %s#%s", type, j);
|
||||
next();
|
||||
}, time);
|
||||
return j;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
var TYPES = ['WRITE', 'READ', 'APPEND'];
|
||||
var chooseAction = function () {
|
||||
var n = rand(100);
|
||||
|
||||
if (n < 50) { return 'APPEND'; }
|
||||
if (n < 90) { return 'READ'; }
|
||||
return 'WRITE';
|
||||
|
||||
//return TYPES[rand(3)];
|
||||
};
|
||||
|
||||
var test = function (script, cb) {
|
||||
var uid = Util.uid();
|
||||
|
||||
var TO_RUN = script.length;
|
||||
var total_run = 0;
|
||||
|
||||
var parallel = 0;
|
||||
var last_run_ordered = -1;
|
||||
//var i = 0;
|
||||
|
||||
var ACTIONS = {};
|
||||
TYPES.forEach(function (type) {
|
||||
ACTIONS[type] = makeAction(type);
|
||||
});
|
||||
|
||||
nThen(function (w) {
|
||||
setTimeout(w(), 3000);
|
||||
// run scripted actions with assertions
|
||||
script.forEach(function (scene) {
|
||||
var type = scene[0];
|
||||
var time = typeof(scene[1]) === 'number'? scene[1]: rand_time();
|
||||
|
||||
var action = ACTIONS[type](time);
|
||||
console.log("Queuing action of type: %s(%s)", type, time);
|
||||
|
||||
var proceed = w();
|
||||
|
||||
switch (type) {
|
||||
case 'APPEND':
|
||||
return schedule.ordered(uid, w(function (next) {
|
||||
parallel++;
|
||||
var temp = action(function () {
|
||||
parallel--;
|
||||
total_run++;
|
||||
proceed();
|
||||
next();
|
||||
});
|
||||
if (temp !== (last_run_ordered + 1)) {
|
||||
throw new Error("out of order");
|
||||
}
|
||||
last_run_ordered = temp;
|
||||
}));
|
||||
case 'WRITE':
|
||||
return schedule.blocking(uid, w(function (next) {
|
||||
parallel++;
|
||||
action(function () {
|
||||
parallel--;
|
||||
total_run++;
|
||||
proceed();
|
||||
next();
|
||||
});
|
||||
if (parallel > 1) {
|
||||
console.log("parallelism === %s", parallel);
|
||||
throw new Error("too much parallel");
|
||||
}
|
||||
}));
|
||||
case 'READ':
|
||||
return schedule.unordered(uid, w(function (next) {
|
||||
parallel++;
|
||||
action(function () {
|
||||
parallel--;
|
||||
total_run++;
|
||||
proceed();
|
||||
next();
|
||||
});
|
||||
}));
|
||||
default:
|
||||
throw new Error("wut");
|
||||
}
|
||||
});
|
||||
}).nThen(function () {
|
||||
// make assertions about the whole script
|
||||
if (total_run !== TO_RUN) {
|
||||
console.log("Ran %s / %s", total_run, TO_RUN);
|
||||
throw new Error("skipped tasks");
|
||||
}
|
||||
console.log("total_run === %s", total_run);
|
||||
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var randomScript = function () {
|
||||
var len = rand(15) + 10;
|
||||
var script = [];
|
||||
while (len--) {
|
||||
script.push([
|
||||
chooseAction(),
|
||||
rand_time(),
|
||||
]);
|
||||
}
|
||||
return script;
|
||||
};
|
||||
|
||||
var WRITE = function (t) {
|
||||
return ['WRITE', t];
|
||||
};
|
||||
var READ = function (t) {
|
||||
return ['READ', t];
|
||||
};
|
||||
|
||||
var APPEND = function (t) {
|
||||
return ['APPEND', t];
|
||||
};
|
||||
|
||||
nThen(function (w) {
|
||||
test([
|
||||
['READ', 150],
|
||||
['APPEND', 200],
|
||||
['APPEND', 100],
|
||||
['READ', 350],
|
||||
['WRITE', 400],
|
||||
['APPEND', 275],
|
||||
['APPEND', 187],
|
||||
['WRITE', 330],
|
||||
['WRITE', 264],
|
||||
['WRITE', 256],
|
||||
], w(function () {
|
||||
console.log("finished pre-scripted test\n");
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
test([
|
||||
WRITE(289),
|
||||
APPEND(281),
|
||||
READ(207),
|
||||
WRITE(225),
|
||||
READ(279),
|
||||
WRITE(300),
|
||||
READ(331),
|
||||
APPEND(341),
|
||||
APPEND(385),
|
||||
READ(313),
|
||||
WRITE(285),
|
||||
READ(304),
|
||||
APPEND(273),
|
||||
APPEND(150),
|
||||
WRITE(246),
|
||||
READ(244),
|
||||
WRITE(172),
|
||||
APPEND(253),
|
||||
READ(215),
|
||||
READ(296),
|
||||
APPEND(281),
|
||||
APPEND(296),
|
||||
WRITE(168),
|
||||
], w(function () {
|
||||
console.log("finished 2nd pre-scripted test\n");
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var totalTests = 50;
|
||||
var randomTests = 1;
|
||||
|
||||
var last = nThen(function () {
|
||||
console.log("beginning randomized tests");
|
||||
});
|
||||
|
||||
var queueRandomTest = function (i) {
|
||||
last = last.nThen(function (w) {
|
||||
console.log("running random test script #%s\n", i);
|
||||
test(randomScript(), w(function () {
|
||||
console.log("finished random test #%s\n", i);
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
while (randomTests <=totalTests) { queueRandomTest(randomTests++); }
|
||||
|
||||
last.nThen(function () {
|
||||
console.log("finished %s random tests", totalTests);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue