gate-executor
Version:
A work queue that can be gated, stopping to wait for sub-queues to complete.
375 lines (302 loc) • 11.7 kB
text/typescript
/* Copyright (c) 2014-2021 Richard Rodger, MIT License */
type Work = {
id: string // Identifier
ge: string // GateExecutor identifier
tm: number // Timeout
dn: string // Description
fn: (callback: () => void) => void
start: number
end: number
gate?: any
callback: () => void
ctxt: { [key: string]: any }
}
// Create root instance. Exported as module.
// * `options` (object): instance options as key-value pairs.
//
// The options are:
// * `interval` (integer): millisecond interval for timeout checks. Default: 111.
// * `timeout` (integer): common millisecond timeout.
// Can be overridden by work item options. Default: 2222.
function MakeGateExecutor(options?: any) {
options = options || {}
options.interval = null == options.interval ? 111 : options.interval
options.timeout = null == options.timeout ? 2222 : options.timeout
return GateExecutor(options, 0)
}
// Create a new instance.
// * `options` (object): instance options as key-value pairs.
// * `instance_counter` (integer): count number of instances created;
// used as identifier.
function GateExecutor(this: any, options: any, instance_counter: number) {
let self: any = {}
self.id = ++instance_counter
self.options = options
// Work queue.
let q: Work[] = []
// Work-in-progress set.
let progress: any = {
// Lookup work by id.
lookup: {},
// Work history - a list of work items in the order executed.
history: [],
}
// List of work items to check for timeouts.
let timeout_checklist: any[] = []
// Internal state.
let s: any = {
// Count of work items added to this instance. Used as generated work identifier.
work_counter: 0,
// When `true`, the instance is in a gated state, and work cannot proceed
// until the gated in-progress work item is completed.
gate: false,
// When `true`, the instance processes work items as they arrive.
// When `false`, no processing happens, and the instance must be started by
// calling the `start` method.
running: false,
// A function called when the work queue and work-in-progress set
// are empty. Set by calling the `clear` method. Will be called
// each time the instance empty.
clear: null,
// A function called once only when the work queue and
// work-in-progress set are first emptied after each start. Set as
// an optional argument to the `start` method.
firstclear: null,
// Timeout interval reference value returned by `setInterval`.
// Timeouts are not checked using `setTimeout`, as it is more
// efficient, and more than sufficient, to check timeouts periodically.
tm_in: null,
hw_tmc: 0,
hw_hst: 0,
}
// Process the next work item.
function processor() {
// If not running, don't process any work items.
if (!s.running) {
return
}
// The timeout interval check is stopped and started only as needed.
if (!self.isclear() && !s.tm_in) {
s.tm_in = setInterval(timeout_check, options.interval)
}
// Process the next work item, returning `true` if there was one.
let next = false
do {
next = false
let work = null
// Remove next work item from the front of the work queue.
if (!s.gate) {
work = q.shift()
}
if (work) {
// Add work item to the work-in-progress set.
progress.lookup[work.id] = work
progress.history.push(work)
s.hw_hst =
progress.history.length > s.hw_hst ? progress.history.length : s.hw_hst
// If work item is a gate, set the state of the instance as
// gated. This work item will need to complete before later
// work items in the queue can be processed.
s.gate = work.gate
// Call the work item function (which does the real work),
// passing a callback. This callback has no arguments
// (including no error!). It is called only to indicate
// completion of the work item. Work items must handle their
// own errors and results.
work.start = Date.now()
work.callback = make_work_fn_callback(work)
timeout_checklist.push(work)
s.hw_tmc =
timeout_checklist.length > s.hw_tmc ? timeout_checklist.length : s.hw_tmc
work.fn(work.callback)
next = true
}
} while (next)
// Keep processing work items until none are left or a gate is reached.
}
// Create the callback for the work function
function make_work_fn_callback(work: any) {
return function work_fn_callback() {
if (work.done) {
return
}
work.end = Date.now()
// Remove the work item from the work-in-progress set. As
// work items may complete out of order, prune the history
// from the front until the first incomplete work
// item. Later complete work items will eventually be
// reached on another processing round.
work.done = true
delete progress.lookup[work.id]
while (progress.history[0] && progress.history[0].done) {
progress.history.shift()
}
while (timeout_checklist[0] && timeout_checklist[0].done) {
timeout_checklist.shift()
}
// If the work item was a gate, it is now complete, and the
// instance can be ungated, allowing later work items in the
// queue to be processed.
if (work.gate) {
s.gate = false
}
// If work queue and work-in-progress set are empty, then
// call the registered clear functions.
if (0 === q.length && 0 === progress.history.length) {
clearInterval(s.tm_in)
s.tm_in = null
if (s.firstclear) {
let fc = s.firstclear
s.firstclear = null
fc()
}
if (s.clear) {
s.clear()
}
}
// Process each work item on next tick to avoid lockups.
setImmediate(processor)
}
}
// To be run periodically via setInterval. For timed out work items,
// calls the done callback to allow work queue to proceed, and marks
// the work item as finished. Work items can receive notification of
// timeouts by providing an `ontm` callback property in the
// work definition object. Work items must handle timeout errors
// themselves, gate-executor cares only for the fact that a timeout
// happened, so it can continue processing.
function timeout_check() {
let now = Date.now()
let work = null
for (let i = 0; i < timeout_checklist.length; ++i) {
work = timeout_checklist[i]
if (!work.gate && !work.done && work.tm < now - work.start) {
if (work.ontm) {
work.ontm(work.tm, work.start, now)
}
work.callback()
}
}
}
// Start processing work items. Must be called to start processing.
// Can be called at anytime, interspersed with calls to other
// methods, including `add`. Takes a function as argument, which is
// called only once on the next time the queues are clear.
self.start = function(firstclear: any) {
// Allow API chaining by not starting in current execution path.
setImmediate(function() {
s.running = true
if (firstclear) {
s.firstclear = firstclear
}
processor()
})
return self
}
// Pause the processing of work items. Newly added items, and items
// not yet started, will not proceed, but items already in progress
// will complete, and the clear function will be called once all in
// progress items finish.
self.pause = function() {
s.running = false
}
// Submit a function that will be called each time there are no more
// work items to process. Multiple calls to this method will replace
// the previously registered clear function.
self.clear = function(done: any) {
s.clear = done
return self
}
// Returns `true` when there are no more work items to process.
self.isclear = function() {
return 0 === q.length && 0 === progress.history.length
}
// Add a work item. This is an object with fields:
// * `fn` (function): the function that performs the work. Takes a
// single argument, the callback function to call when the work is
// complete. THis callback does **not** accept errors or
// results. It's only purpose is to indicate that the work is
// complete (whether failed or not). The work function itself must
// handle callbacks to the application. Required.
// * `id` (string): identifier for the work item. Optional.
// * `tm` (integer): millisecond timeout specific to this work item,
// overrides general timeout. Optional.
// * `ontm` (function): callback to indicate work item timeout. Optional.
// * `dn` (string): description of the work item, used in the
// state description. Optional.
self.add = function(work: Work) {
s.work_counter += 1
work.id = work.id || '' + s.work_counter
work.ge = self.id
work.tm = null == work.tm ? options.timeout : work.tm
work.dn = work.dn || work.fn.name || '' + Date.now()
// Used by calling code to store additional context.
work.ctxt = {}
q.push(work)
if (s.running) {
// Work items are **not** processed in the current execution path!
// This prevents lockup, and avoids false positives in unit tests.
// Work items are assumed to be inherently asynchronous.
setImmediate(processor)
}
return self
}
// Create a new gate. Returns a new `GateExecutor` instance. All
// work items added to the new instance must complete before the
// gate is cleared, and work items in the queue can be processed. A
// gate is cleared when the new instance is **first** cleared. Work
// items subsequently added to the new instance are not considered
// part of the gate. Gates can extend to any depth and form a tree
// structure that requires breadth-first traversal in terms of the
// work item queue. Gates do not have timeouts, and can only be
// cleared when all added work items complete.
self.gate = function() {
let ge: any = GateExecutor(options, instance_counter)
let fn = function gate(done: any) {
// This is the work function of the gate, which starts the new
// instance, and considers the gate work item complete when the
// work queue clears for the first time.
ge.start(done)
}
self.add({ gate: ge, fn: fn })
return ge
}
// Return a data structure describing the current state of the work
// queues, and organised as a tree structure indicating the gating
// relationships.
self.state = function() {
let out: any = []
// First list any in-progress work items.
for (let hI = 0; hI < progress.history.length; ++hI) {
let pe = progress.history[hI]
if (!pe.done) {
out.push({ s: 'a', ge: pe.ge, dn: pe.dn, id: pe.id })
}
}
// Then list any waiting work items.
for (let qI = 0; qI < q.length; ++qI) {
let qe = q[qI]
if (qe.gate) {
// Go down a level when there's a gate.
out.push(qe.gate.state())
} else {
out.push({ s: 'w', ge: qe.ge, dn: qe.dn, id: qe.id })
}
}
out.internal = {
qlen: q.length,
hlen: progress.history.length,
klen: Object.keys(progress.lookup).length,
tlen: timeout_check.length,
hw_hst: s.hw_hst,
hw_tmc: s.hw_tmc,
}
return out
}
return self
}
// The module function
export default MakeGateExecutor
if (undefined != typeof (module.exports)) {
module.exports = MakeGateExecutor
}