-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
feat: Canceling flows #1542
feat: Canceling flows #1542
Conversation
This is ready! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably the change I'm looking forward to the most in v3
, really excited about it!
The behaviour seems a little unexpected though. I assumed the final callback would still be called, with partial results, similar to the way breakLoop
works for the collection methods?
I think the only function missing is eachOfArrayLike
in lib/eachOf
. Other than that, should retry/retryable
also be cancelable?
@@ -189,6 +190,10 @@ export default function (tasks, concurrency, callback) { | |||
|
|||
var taskCallback = onlyOnce(function(err, result) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Higher up in the runTask
function, we should change if (hasError) return;
to if (hasError || canceled) return;
so other tasks, not dependent on this one, won't continue running.
lib/waterfall.js
Outdated
@@ -75,6 +76,10 @@ export default function(tasks, callback) { | |||
} | |||
|
|||
function next(err/*, ...args*/) { | |||
if (err === false || canceled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think this can be simplified to if (err === false) return
. next
is wrapped in onlyOnce
so I don't believe there's a situation were it would be called twice.
@@ -168,6 +168,31 @@ describe('auto', function () { | |||
setTimeout(done, 100); | |||
}); | |||
|
|||
it('auto canceled', function(done){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also add a test for ensuring other functions don't get started when one is canceled. Something along the lines of
it('does not start other tasks when it has been canceled', function(done) {
const call_order = []
async.auto({
task1: function(callback) {
call_order.push(1);
// defer calling task2, so task3 has time to stop execution
async.setImmediate(callback);
},
task2: ['task1', function(/*results, callback*/) {
call_order.push(2);
throw new Error('task2 should not be called');
}],
task3: function(callback) {
call_order.push(3);
callback(false);
},
task4: ['task3', function(/*results, callback*/) {
call_order.push(4);
throw new Error('task4 should not be called');
}]
},
function() {
throw new Error('should not get here')
});
setTimeout(() => {
expect(call_order).to.eql([1,3])
done()
}, 25)
});
@@ -34,6 +34,22 @@ describe('during', function() { | |||
); | |||
}); | |||
|
|||
it('during canceling', (done) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also add a test for canceling during
in the check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're going to deprecate during, making all whilst/until functions have an async test function, so no need for comprehensive test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some minor nits but looks promising overall, I'm excited to try and find uses for this in my projects
intro.md
Outdated
@@ -173,6 +173,40 @@ async.map([1, 2, 3], AsyncSquaringLibrary.square.bind(AsyncSquaringLibrary), fun | |||
}); | |||
``` | |||
|
|||
### Subtle Memory Leaks | |||
|
|||
There are cases where you might want to exit early from async flow, when calling an Async method insice another async function: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo -insice/+inside
lib/internal/eachOfLimit.js
Outdated
else if (value === breakLoop || (done && running <= 0)) { | ||
done = true; | ||
if (canceled) return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move to top of iterateeCallback
for simplicity? (it would also let you avoid the !canceled
check in the error condition)
@@ -102,6 +102,7 @@ export default function (tasks, concurrency, callback) { | |||
|
|||
var results = {}; | |||
var runningTasks = 0; | |||
var canceled = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo should be cancelled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either spelling is correct. I chose one 'l' for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
damn americans
Ok, I believe I've addressed all your concerns. Thanks for the review! And yes, I believe not calling the final callback is what we want here, as a mechanism to avoid memory leaks. In what cases would you want to break iteration, and have the final callback called? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you've already merged this but can we try to be consistent about semi-colon usage please.
|
||
it('does not start other tasks when it has been canceled', function(done) { | ||
const call_order = [] | ||
debugger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Misc debugger statement
That's a good point @aearly. Thanks for making the changes, lgtm! |
@aearly In my code, the final callback of a waterfall calls the parent callback, so it would break my existing code to use this cancel feature. For example:
Is this not a use case for others? |
This feature breaks the basic node API, passing false to the callback should not be treated differently than |
@aearly Can you take a look at @nathaniel-holder and @yeya comment? |
Closes #1064
This allows you to signal that you have exited early from an async flow by
callback(false)
--- calling back withfalse
as the error. All further callbacks will be ignored, and the final callback will not be called. This is handy to avoid a subtle memory leak when you exit early.eachOfLimit
- based methods (e.g. all collection-based methods)compose
/seq
waterfall
auto
/autoInject
whilst
,until
, and familyforever