Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel and restart long running process on file change by using context.WithCancel(..) #60

Merged
merged 7 commits into from
Aug 4, 2017

Conversation

JackMordaunt
Copy link
Contributor

@JackMordaunt JackMordaunt commented Aug 4, 2017

Tasks are now called in a goroutine meaning the order of completion is not guaranteed.
I'm not sure if task order is a requirement.

The reason they have to be called in a goroutine is because long running tasks would block watchTasks(..), such that it doesn't read-in file change events.

The goroutines don't leak because the context.CancelFunc cancels the currently executing Task, causing Executor.RunTask(..) to return, thus exiting the goroutine.

I tested it locally and it appears to work correctly: long running processes are cancelled and re-started when a file change event occurs.

Resolves #60

Task are now called in a goroutine meaning the order of completion is not guaranteed.
@andreynering andreynering added the type: enhancement A change to an existing feature or functionality. label Aug 4, 2017
watch.go Outdated
break
}
// Use of goroutines means task order is not guaranteed.
// Not sure if task order is a requirement. (jackmordaunt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running them in parallel makes sense to me. @andreynering?

watch.go Outdated
// Not sure if task order is a requirement. (jackmordaunt)
go func(a string) {
ctx, cancel := context.WithCancel(context.Background())
taskCancellers[a] = cancel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we create the context and update taskCancellers[a] outside of the concurrent go function. Modifying a map concurrently without a mutex is subject to data-race conditions or worse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some thought... Could we also actually simplify this just use the same context (instead of taskCancellers map), and cancel all tasks through the same cancel function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea to use a single context. Updated the pull request to do just that.

watch.go Outdated
}
// Use of goroutines means task order is not guaranteed.
// Not sure if task order is a requirement. (jackmordaunt)
go func(a string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to pass along a, you could capture it instead.

for _, a := range args {
    a := a // capture range variable
    ...
}

This is relevant to change due to my next comment on line 23.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, are there any downsides to passing along the variable other than memory allocations?

watch.go Outdated
@@ -41,11 +47,21 @@ loop:
select {
case <-watcher.Events:
for _, a := range args {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we just loop through taskCancellers directly? If it's set in a non-concurrent manner, we don't need as many checks as here.

watch.go Outdated
@@ -41,11 +47,21 @@ loop:
select {
case <-watcher.Events:
for _, a := range args {
if err := e.RunTask(context.Background(), Call{Task: a}); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as taskCancellers is modified concurrently, this is subject to data-race, and some tasks might (theoretically) be started after ok returns false.

Removed unnecessary map of contexts in place of a single context.
This avoids concurrent map access and redundant iteration.
I accidentally removed the goroutine surrounding the initial task executions.
Added it back in this commit.
@JackMordaunt
Copy link
Contributor Author

JackMordaunt commented Aug 4, 2017

With 1175c01, task outputs noisy 'error' messages when using watch flag:
task: Failed to run task "build": context canceled
task: Failed to run task "build": exit status 255
Should this message be silenced, since it is actually intended behaviour and not an error?

Removed comment.
Capturing range variable instead of passing it to groutine.
@andreynering
Copy link
Member

@JackMordaunt This should do the trick. Print only if it's not a context error:

switch err {
case context.Canceled, context.DeadlineExceeded:
        // supress error
default:
        e.println(err)
}

It's also missing args assigning:

for _, a := range args {
        a := a // this is important to prevent goroutine using the value of the next iteration

@andreynering
Copy link
Member

@JackMordaunt Great work, thank you very much!

@andreynering andreynering merged commit ddd063f into go-task:master Aug 4, 2017
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A change to an existing feature or functionality.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants