Add descriptive error message when there is a cyclic dependancy (#9)

* add descriptive error message when there is a cyclic dependancy

* Change files
This commit is contained in:
Brian Cheruiyot 2021-04-13 09:46:06 +03:00 коммит произвёл GitHub
Родитель 4fe1927648
Коммит 123d9fa714
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 69 добавлений и 17 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
node_modules node_modules
lib lib
*.log *.log
.idea

Просмотреть файл

@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "add descriptive error message when there is a cyclic dependancy",
"packageName": "p-graph",
"email": "cheruiyotbryan@gmail.com",
"dependentChangeType": "patch",
"date": "2021-04-09T12:05:29.808Z"
}

Просмотреть файл

@ -37,8 +37,11 @@ export class PGraph {
throw new Error("We could not find a node in the graph with no dependencies, this likely means there is a cycle including all nodes"); throw new Error("We could not find a node in the graph with no dependencies, this likely means there is a cycle including all nodes");
} }
if (graphHasCycles(this.pGraphDependencyMap)) { const hasCycles = graphHasCycles(this.pGraphDependencyMap);
throw new Error("The dependency graph has a cycle in it");
if (hasCycles.hasCycle) {
const { nodeId, dependsOn, dependedOnBy } = hasCycles.details;
throw new Error(`The dependency graph has a cycle at ${nodeId} which depends on ${dependsOn} and is depended on by ${dependedOnBy}`);
} }
} }
@ -81,7 +84,7 @@ export class PGraph {
} finally { } finally {
// schedule next round of tasks if options.continue (continue on error) or successfully run task // schedule next round of tasks if options.continue (continue on error) or successfully run task
const shouldScheduleMoreTasks = options?.continue || !taskToRun.failed; const shouldScheduleMoreTasks = options?.continue || !taskToRun.failed;
if (shouldScheduleMoreTasks) { if (shouldScheduleMoreTasks) {
// "currentlyRunningTaskCount" cannot be decremented on non-continue cases because of async nature of // "currentlyRunningTaskCount" cannot be decremented on non-continue cases because of async nature of
// the queue runner. The race condition will end up appearing as if there was no failures even though // the queue runner. The race condition will end up appearing as if there was no failures even though
@ -102,7 +105,7 @@ export class PGraph {
if (dependentNode.dependsOn.size === 0) { if (dependentNode.dependsOn.size === 0) {
priorityQueue.insert(dependentId, nodeCumulativePriorities.get(dependentId)!); priorityQueue.insert(dependentId, nodeCumulativePriorities.get(dependentId)!);
} }
}); });
} }
} }
}; };
@ -120,7 +123,7 @@ export class PGraph {
} }
return; return;
} }
while (!priorityQueue.isEmpty() && (concurrency === undefined || currentlyRunningTaskCount < concurrency)) { while (!priorityQueue.isEmpty() && (concurrency === undefined || currentlyRunningTaskCount < concurrency)) {
scheduleTask() scheduleTask()
.then(() => trySchedulingTasks()) .then(() => trySchedulingTasks())
@ -128,7 +131,7 @@ export class PGraph {
errors.push(e); errors.push(e);
// if a continue option is set, this merely records what errors have been encountered // if a continue option is set, this merely records what errors have been encountered
// it'll continue down the execution until all the tasks that still works // it'll continue down the execution until all the tasks that still works
if (options?.continue) { if (options?.continue) {
trySchedulingTasks(); trySchedulingTasks();
} else { } else {

Просмотреть файл

@ -187,7 +187,7 @@ describe("Public API", () => {
expect(() => pGraph(nodeMap, dependencies)).toThrow(); expect(() => pGraph(nodeMap, dependencies)).toThrow();
}); });
it("throws an exception when the dependency graph has a cycle", async () => { it("throws an exception with detailed message when the dependency graph has a cycle", async () => {
// This is almost the same as the last test, except the root node is not a part of the cycle // This is almost the same as the last test, except the root node is not a part of the cycle
const nodeMap: PGraphNodeMap = new Map([ const nodeMap: PGraphNodeMap = new Map([
["A", { run: () => Promise.resolve() }], ["A", { run: () => Promise.resolve() }],
@ -202,8 +202,8 @@ describe("Public API", () => {
["C", "D"], ["C", "D"],
["D", "B"], ["D", "B"],
]; ];
const expectedErrorMessage = "The dependency graph has a cycle at B which depends on A,D and is depended on by C";
expect(() => pGraph(nodeMap, dependencies)).toThrow(); expect(() => pGraph(nodeMap, dependencies)).toThrow(expectedErrorMessage);
}); });
it("resolves an empty dependnecy graph", async () => { it("resolves an empty dependnecy graph", async () => {

Просмотреть файл

@ -1,9 +1,10 @@
import { PGraphNodeWithDependencies } from "./types"; import { PGraphNodeWithCyclicDependency, PGraphNodeWithNoCyclicDependency, PGraphNodeWithDependencies } from "./types";
/** /**
* Checks for any cycles in the dependency graph, returning false if no cycles were detected. * Checks for any cycles in the dependency graph, returning `{ hasCycle: false }` if no cycles were detected.
* Otherwise it returns the details of where the cycle was detected.
*/ */
export function graphHasCycles(pGraphDependencyMap: Map<string, PGraphNodeWithDependencies>): boolean { export function graphHasCycles(pGraphDependencyMap: Map<string, PGraphNodeWithDependencies>): PGraphNodeWithCyclicDependency | PGraphNodeWithNoCyclicDependency {
/** /**
* A map to keep track of the visited and visiting nodes. * A map to keep track of the visited and visiting nodes.
* <node, true> entry means it is currently being visited. * <node, true> entry means it is currently being visited.
@ -12,7 +13,7 @@ export function graphHasCycles(pGraphDependencyMap: Map<string, PGraphNodeWithDe
*/ */
const visitMap = new Map<string, boolean>(); const visitMap = new Map<string, boolean>();
for (const [nodeId] of pGraphDependencyMap.entries()) { for (const [nodeId, nodes] of pGraphDependencyMap.entries()) {
/** /**
* Test whether this node has already been visited or not. * Test whether this node has already been visited or not.
*/ */
@ -20,13 +21,20 @@ export function graphHasCycles(pGraphDependencyMap: Map<string, PGraphNodeWithDe
/** /**
* Test whether the sub-graph of this node has cycles. * Test whether the sub-graph of this node has cycles.
*/ */
if (hasCycleDFS(pGraphDependencyMap, visitMap, nodeId)) { if (hasCycleDFS(pGraphDependencyMap, visitMap, nodeId)) {
return true; return {
hasCycle: true,
details: {
nodeId,
dependsOn: Array.from(nodes.dependsOn),
dependedOnBy: Array.from(nodes.dependedOnBy)
}
};
} }
} }
} }
return false; return { hasCycle: false };
} }
/** /**

Просмотреть файл

@ -52,3 +52,35 @@ export interface PGraphNodeWithDependencies extends PGraphNode {
*/ */
failed: boolean; failed: boolean;
} }
export interface PGraphNodeWithNoCyclicDependency {
/**
* Flag whether there is no cyclic dependency
*/
hasCycle: false;
}
export interface PGraphNodeWithCyclicDependency {
/**
* Flag whether there is a cyclic dependency
*/
hasCycle: true;
/**
* Details on where the cyclic dependency was detected.
*/
details: {
/**
* The identifier of this node, where the cyclic dependency was detected
*/
nodeId: string,
/**
* The set of nodes that this node depends on.
*/
dependsOn: string[];
/**
* The set of nodes that depend on this node.
*/
dependedOnBy: string[];
}
}