Skip to content

Commit 19585c1

Browse files
committed
Merge pull request #1140 from b-paul/fix-#1092
Fix #1092
2 parents 77f6a80 + 83bd8af commit 19585c1

File tree

2 files changed

+63
-18
lines changed

2 files changed

+63
-18
lines changed

lib/auto.js

+46-18
Original file line numberDiff line numberDiff line change
@@ -112,36 +112,33 @@ export default function (tasks, concurrency, callback) {
112112

113113
var readyTasks = [];
114114

115+
// for cycle detection:
116+
var readyToCheck = []; // tasks that have been identified as reachable
117+
// without the possibility of returning to an ancestor task
118+
var uncheckedDependencies = {};
115119

116120
forOwn(tasks, function (task, key) {
117121
if (!isArray(task)) {
118122
// no dependencies
119123
enqueueTask(key, [task]);
124+
readyToCheck.push(key);
120125
return;
121126
}
122127

123128
var dependencies = task.slice(0, task.length - 1);
124129
var remainingDependencies = dependencies.length;
125-
126-
checkForDeadlocks();
127-
128-
function checkForDeadlocks() {
129-
var len = dependencies.length;
130-
var dep;
131-
while (len--) {
132-
if (!(dep = tasks[dependencies[len]])) {
133-
throw new Error('async.auto task `' + key +
134-
'` has non-existent dependency in ' +
135-
dependencies.join(', '));
136-
}
137-
if (isArray(dep) && indexOf(dep, key, 0) >= 0) {
138-
throw new Error('async.auto task `' + key +
139-
'`Has cyclic dependencies');
140-
}
141-
}
130+
if (!remainingDependencies) {
131+
enqueueTask(key, [task]);
132+
readyToCheck.push(key);
142133
}
134+
uncheckedDependencies[key] = remainingDependencies;
143135

144136
arrayEach(dependencies, function (dependencyName) {
137+
if (!tasks[dependencyName]) {
138+
throw new Error('async.auto task `' + key +
139+
'` has a non-existent dependency in ' +
140+
dependencies.join(', '));
141+
}
145142
addListener(dependencyName, function () {
146143
remainingDependencies--;
147144
if (remainingDependencies === 0) {
@@ -151,9 +148,9 @@ export default function (tasks, concurrency, callback) {
151148
});
152149
});
153150

151+
checkForDeadlocks();
154152
processQueue();
155153

156-
157154
function enqueueTask(key, task) {
158155
readyTasks.push(function () {
159156
runTask(key, task);
@@ -222,5 +219,36 @@ export default function (tasks, concurrency, callback) {
222219
}
223220
}
224221

222+
function checkForDeadlocks() {
223+
// Kahn's algorithm
224+
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
225+
// http://connalle.blogspot.com/2013/10/topological-sortingkahn-algorithm.html
226+
var currentTask;
227+
var counter = 0;
228+
while (readyToCheck.length) {
229+
currentTask = readyToCheck.pop();
230+
counter++;
231+
arrayEach(getDependents(currentTask), function (dependent) {
232+
if (!(--uncheckedDependencies[dependent])) {
233+
readyToCheck.push(dependent);
234+
}
235+
});
236+
}
237+
238+
if (counter !== numTasks) {
239+
throw new Error(
240+
'async.auto cannot execute tasks due to a recursive dependency'
241+
);
242+
}
243+
}
225244

245+
function getDependents(taskName) {
246+
var result = [];
247+
forOwn(tasks, function (task, key) {
248+
if (isArray(task) && indexOf(task, taskName, 0) >= 0) {
249+
result.push(key);
250+
}
251+
});
252+
return result;
253+
}
226254
}

mocha_test/auto.js

+17
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,23 @@ describe('auto', function () {
305305
done();
306306
});
307307

308+
// Issue 1092 on github: https://github.com/caolan/async/issues/1092
309+
it('extended cycle detection', function(done) {
310+
var task = function (name) {
311+
return function (results, callback) {
312+
callback(null, 'task ' + name);
313+
};
314+
};
315+
expect(function () {
316+
async.auto({
317+
a: ['c', task('a')],
318+
b: ['a', task('b')],
319+
c: ['b', task('c')]
320+
});
321+
}).to.throw();
322+
done();
323+
});
324+
308325
// Issue 988 on github: https://github.com/caolan/async/issues/988
309326
it('auto stops running tasks on error', function(done) {
310327
async.auto({

0 commit comments

Comments
 (0)