You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
6.0 KiB
JavaScript
233 lines
6.0 KiB
JavaScript
"use strict";
|
|
|
|
const Promise = require("bluebird");
|
|
const isEndOfStream = require("@promistream/is-end-of-stream");
|
|
const isAborted = require("@promistream/is-aborted");
|
|
const propagateAbort = require("@promistream/propagate-abort");
|
|
const pipe = require("@promistream/pipe");
|
|
const sequentialize = require("@promistream/sequentialize");
|
|
const defaultValue = require("default-value");
|
|
const debug = require("debug")("promistream:parallelize");
|
|
|
|
const createPromiseListener = require("./promise-listener");
|
|
|
|
// FIXME: Verify that if an EndOfStream or Aborted marker comes in, it is left queued up until all of the in-flight non-marker results have been processed; otherwise the downstream may erroneously believe that the stream has already ended, while more items are still on their way
|
|
|
|
module.exports = function parallelizeStream(threadCount, options = {}) {
|
|
let ordered = defaultValue(options.ordered, true);
|
|
|
|
/* TODO: Does this need a more efficient FIFO queue implementation? */
|
|
let signals = [];
|
|
let storedErrors = [];
|
|
let queueListener = createPromiseListener();
|
|
let parallelMode = true;
|
|
let filling = false;
|
|
let currentSource = null;
|
|
|
|
function fillRequest() {
|
|
if (parallelMode === true && signals.length < threadCount) {
|
|
return Promise.try(() => {
|
|
return currentSource.peek();
|
|
}).then((valueAvailable) => {
|
|
if (valueAvailable) {
|
|
queueRead();
|
|
|
|
return fillRequest(currentSource);
|
|
} else {
|
|
return switchToSequentialMode();
|
|
}
|
|
});
|
|
} else {
|
|
debug(`Paused internal queue fill (parallel mode = ${parallelMode}, thread count = ${threadCount}, in-flight requests = ${signals.length})`);
|
|
filling = false;
|
|
}
|
|
}
|
|
|
|
function tryStartFilling() {
|
|
if (!filling) {
|
|
debug("Starting internal queue fill...");
|
|
filling = true;
|
|
|
|
return fillRequest(currentSource);
|
|
}
|
|
}
|
|
|
|
function switchToParallelMode() {
|
|
debug("Switching to parallel mode");
|
|
parallelMode = true;
|
|
|
|
return tryStartFilling();
|
|
}
|
|
|
|
function switchToSequentialMode() {
|
|
debug("Switching to sequential mode");
|
|
parallelMode = false;
|
|
}
|
|
|
|
function bufferNotEmpty() {
|
|
/* NOTE: This should *only* take into account items that have not been peeked yet! */
|
|
let peekedSignal;
|
|
|
|
if (ordered) {
|
|
/* This will return only if there is a contiguous sequence of settled signals from the start, of which *at least one* has not been peeked yet. */
|
|
for (let signal of signals) {
|
|
if (signal.trackingPromise.isPending()) {
|
|
break;
|
|
} else if (signal.peeked) {
|
|
continue;
|
|
} else {
|
|
/* Settled, and not peeked yet. */
|
|
peekedSignal = signal;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
peekedSignal = signals.find((signal) => {
|
|
return !(signal.trackingPromise.isPending()) && !signal.peeked;
|
|
});
|
|
}
|
|
|
|
if (peekedSignal != null) {
|
|
peekedSignal.peeked = true;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function awaitInFlightRequests() {
|
|
if (signals.length > 0) {
|
|
return true;
|
|
} else {
|
|
debug("Waiting for queue to be non-empty...");
|
|
|
|
return Promise.try(() => {
|
|
return queueListener.listen();
|
|
}).tap(() => {
|
|
debug("Got queue-filled notification");
|
|
}).tapCatch((error) => {
|
|
debug(`Queue listener rejected: ${error.stack}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
function queueRead() {
|
|
let promise = Promise.try(() => {
|
|
return currentSource.read();
|
|
});
|
|
|
|
let signalObject = { promise: promise };
|
|
|
|
signals.push({
|
|
peeked: false,
|
|
object: signalObject,
|
|
trackingPromise: Promise.try(() => {
|
|
return promise.reflect();
|
|
}).then(() => {
|
|
return signalObject;
|
|
})
|
|
});
|
|
|
|
queueListener.resolve();
|
|
}
|
|
|
|
function awaitResult() {
|
|
return Promise.try(() => {
|
|
return awaitInFlightRequests();
|
|
}).then(() => {
|
|
debug("Awaiting next finished result...");
|
|
if (ordered) {
|
|
return signals[0].trackingPromise;
|
|
} else {
|
|
return Promise.race(signals.map((item) => item.trackingPromise));
|
|
}
|
|
}).then((signalObject) => {
|
|
let resultPromise = signalObject.promise;
|
|
|
|
signals = signals.filter((signal) => (signal.object !== signalObject));
|
|
|
|
let isRejected = resultPromise.isRejected();
|
|
let isEndOfStream_ = isRejected && isEndOfStream(resultPromise.reason());
|
|
let isAborted_ = isRejected && isAborted(resultPromise.reason());
|
|
|
|
if (isEndOfStream_ || isAborted_) {
|
|
switchToSequentialMode();
|
|
|
|
if (signals.length > 0) {
|
|
/* Throw away the marker, and wait for the next result */
|
|
return awaitResult();
|
|
} else {
|
|
/* Queue has been exhausted, so this marker will be the final result; pass it through */
|
|
return signalObject.promise;
|
|
}
|
|
} else {
|
|
return signalObject.promise;
|
|
}
|
|
});
|
|
}
|
|
|
|
let parallelizer = {
|
|
_promistreamVersion: 0,
|
|
description: `parallelize (${threadCount} threads)`,
|
|
abort: propagateAbort,
|
|
peek: function (source) {
|
|
return Promise.try(() => {
|
|
debug("Processing peek...");
|
|
|
|
if (bufferNotEmpty()) {
|
|
return true;
|
|
} else {
|
|
if (parallelMode === true) {
|
|
return source.peek();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
read: function (source) {
|
|
return Promise.try(() => {
|
|
debug("Processing read...");
|
|
currentSource = source;
|
|
|
|
if (storedErrors.length > 0) {
|
|
throw storedErrors.shift();
|
|
} else {
|
|
if (parallelMode) {
|
|
/* This runs in the background, potentially perpetually */
|
|
Promise.try(() => {
|
|
return tryStartFilling();
|
|
}).catch((err) => {
|
|
queueListener.reject(err);
|
|
debug(`Error occurred during filling: ${err.stack}`);
|
|
// storedErrors.push(err);
|
|
});
|
|
|
|
return awaitResult();
|
|
} else {
|
|
/* Sequential mode */
|
|
if (signals.length > 0) {
|
|
/* Clear out the remaining in-flight reads from the previous parallel-mode operation, first. */
|
|
return awaitResult();
|
|
} else {
|
|
debug("Passing through read to upstream...");
|
|
|
|
return Promise.try(() => {
|
|
return source.read();
|
|
}).then((result) => {
|
|
switchToParallelMode();
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
return pipe([
|
|
parallelizer,
|
|
sequentialize()
|
|
]);
|
|
};
|