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

WIP: Run final targets after user cancels with ctrl+c #1946

Merged
merged 2 commits into from
May 21, 2018

Conversation

kblohm
Copy link
Contributor

@kblohm kblohm commented May 20, 2018

Hi,
i am not sure if this is needed/wanted but i wanted to try it out ;).
When the user presses ctrl+c, all running tasks are cancelled and all already started processes are killed. Afterwards the finalTargets are run, unless the user presses ctrl+c again to forcefully cancel.
I am also not sure if this is the right approach, but maybe you can have a look.
Some of the changes are just so that i can #load the target-module and test stuff.

@matthid
Copy link
Member

matthid commented May 20, 2018

Clever, I had a similar idea in mind (just killing all processes). The only thing this doesn't handle is when the target function itself is still doing something (and not waiting for external functions) or if the target is just running the next process after we killed the previous one (some tasks have an internal retry...).

In fact, I feel like in practice the solution you propose is already more than useful - maybe we can kind of fall-back to our current behavior when the user presses ctrl+c a second time (or the shell might be killing us anyway in that case). Good thing is that the token is available via the context, so people can "fix"/adapt their target code if needed.

@kblohm
Copy link
Contributor Author

kblohm commented May 20, 2018

What is the current behavior? Istn that just letting the shell kill the process? Or is there already some cleanup-code i did not manage to find?

@matthid
Copy link
Member

matthid commented May 20, 2018

No I think the shell is just killing stuff. However if msbuild is running it prints "attempting to kill..." or something like that so the signal already seems to flow though fake (but on the other side I have no idea how things work behind the scenes)

@kblohm kblohm force-pushed the handleUserCancel branch from 2714e29 to c36795f Compare May 20, 2018 22:48
@kblohm kblohm force-pushed the handleUserCancel branch from c36795f to c1c5acf Compare May 21, 2018 12:33
Copy link
Member

@matthid matthid left a comment

Choose a reason for hiding this comment

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

Looks very very reasonable. Some suggestions/question in-line

f a)

let cancelHandler = captureContext (handleUserCancelEvent cts)
use __ = Console.CancelKeyPress |> Observable.subscribeOnce cancelHandler
Copy link
Member

Choose a reason for hiding this comment

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

Does this throw in non-interactive mode (CI systems?) or just do nothing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dont know. As no one is pressing any keys there, that should just work. Do you have any idea on how to test that?

Copy link
Member

Choose a reason for hiding this comment

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

We can keep this in mind and test on staging

@@ -584,15 +596,16 @@ module Target =
member __.GetNextTarget (ctx) = mbox.PostAndAsyncReply(fun reply -> GetNextTarget(ctx, reply))
}

let runOptimal workerNum (order:Target[] list) targetContext =
let runOptimal workerNum (order:Target[] list) targetContext=
Copy link
Member

Choose a reason for hiding this comment

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

add that space again :)

@@ -159,10 +167,12 @@ module Target =
Trace.traceError <| sprintf " - %s" target.Value.Name
failwithf "Target \"%s\" is not defined." name

let internal runSimpleInternal context target =
Copy link
Member

Choose a reason for hiding this comment

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

space

Arguments : string list
IsRunningFinalTargets : bool
CancellationToken : CancellationToken }
static member Create ft all args isRunningFinalTargets (token:CancellationToken)= {
Copy link
Member

Choose a reason for hiding this comment

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

Just start with IsRunningFinalTargets = false? (also spacing around =)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I need a formatting tool ;). you mean IsRunningFinalTargets as the first argument? Is the bool OK or should that be an enum with all the possible states?

Copy link
Member

Choose a reason for hiding this comment

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

bool is okish, but Create doesn't need it as argument is what I suggest

member __.OnCompleted()=
remove()
}
observable.Subscribe observer
Copy link
Member

Choose a reason for hiding this comment

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

So this is basically observable.Take(1), correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, should be Take(1)/First. But i did not want to add an extra library

Copy link
Member

Choose a reason for hiding this comment

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

I think this has a race when the OnNext event is triggered before setRemover is called

Copy link
Member

Choose a reason for hiding this comment

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

Trying to find a suggestion ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isnt this pretty much the same as here ?

Copy link
Member

Choose a reason for hiding this comment

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

Yes and in fact I feel like it has the same problem. It probably will just work for our use case.

Copy link
Contributor Author

@kblohm kblohm May 21, 2018

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Yes actually I have no problem with adding that dependency as I'm not able to come up with a solution and give up :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How important is .netstandard1.6 support? I would have to remove that.

Copy link
Member

@matthid matthid May 21, 2018

Choose a reason for hiding this comment

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

I'm pretty sure we can live without netstandard1.6

// always try to keep "parallelJobs" runners busy
ParallelRunner.runOptimal parallelJobs order context
//order
// |> Seq.fold (fun context par -> runTargetsParallel parallelJobs par context) context
Copy link
Member

Choose a reason for hiding this comment

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

I think we can remove this now as well as the implementation if it still exists (though I can see if you don't want to do it as part of this PR - I wouldn't mind if we already do changes here)

Copy link
Member

Choose a reason for hiding this comment

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

(Take this with caution:) Maybe we even can unify this now with the other branch (though I'm not sure if that actually looks better)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean remove the ParallelRunner.runOptimal method and the whole if parallelJobs > 1 && not singleTarget ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would then remove the whole ParallelRunner as far as i can see.

Copy link
Member

Choose a reason for hiding this comment

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

No, I meant the comments and runTargetsParallel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, i can remove those. That confused me ;)

let mutable finalTargetResult = 0
Target.createFinal "Final" (fun _ -> finalTargetResult <- 1)

let context = run "c"
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should let some tests run with a parallel version of run

Copy link
Contributor Author

@kblohm kblohm May 21, 2018

Choose a reason for hiding this comment

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

I was actually going to add tests for the cancellation, but i am not sure how to do that. It would probaby have to run in another Process? And since i did not really manage to do that i at least added some tests for the normal final-targets :/.
CoreFx has some for cancellation here. But that is quite a lot of boilerblate to add.

Copy link
Member

Choose a reason for hiding this comment

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

I think cancellation is fine, I meant is just letting it run through the parallel code paths and see if the result still is fine (yes in fact that might mean that stuff is our of order if dependencies allow it but we can still do basic checks)

let mutable failureTargetResult = 0
Target.createBuildFailure "FailureTarget" (fun _ -> failureTargetResult <- 1)
Target.createBuildFailure "FailureTarget2" (fun _ -> failureTargetResult <- failureTargetResult+1)
Target.activateBuildFailure "FailureTarget"
Copy link
Member

Choose a reason for hiding this comment

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

As far as I understand we have to activate manually because people use this as part of other targets (it they activate FailureTarget as part of target b for example.

In an ideal world I'm thinking about doing that over the context and not via global mutable state - and in fact "freeze" the state once "run" is called. However I don't think we can do this at this point without breaking change.
Sadly that one slipped through when re-designing the API... Something to keep in mind for fake 6

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we need tests then to cover activation in another target? That would also only work at specific timings, so yeah that looks kinda strange.

let mgr = createCtxMgr order targetContext
let targetRunner () =
async {
let token = targetContext.CancellationToken
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of using the token from the context here, should i add it to Async.StartAsTask and Async.RunSynchronously ? I guess i would have to add it to both because of the Task.AwaitAll?

Copy link
Member

Choose a reason for hiding this comment

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

Actually I think it is fine as it is, using the default cancellation token has funny side effects. We want the runner to stay alive in order to execute the final targets.

@matthid
Copy link
Member

matthid commented May 21, 2018

I'll merge this and some some tests for the parallel branch. Or are you still working on something right now?

@kblohm
Copy link
Contributor Author

kblohm commented May 21, 2018

yes, i am fixing all the stuff we discussed

@kblohm
Copy link
Contributor Author

kblohm commented May 21, 2018

I am running the build locally, if that passes i will commit. Do you want a new commit so you can see the changes easier? Otherwise i will ammend.

@matthid
Copy link
Member

matthid commented May 21, 2018

As you like ;) More commits lead to more contribution points on github :P

@kblohm
Copy link
Contributor Author

kblohm commented May 21, 2018

Everything for internetpoints!

@matthid matthid changed the base branch from master to release/rc May 21, 2018 18:57
@kblohm
Copy link
Contributor Author

kblohm commented May 21, 2018

Things we could still do:

  • Kill running processes when aborting final targets
  • Kill process-tree (i dont think Process.killAllCreatedProcesses() kills childprocesses)
  • Handle ctrl+c or at least killing of processes in runSimple

@matthid
Copy link
Member

matthid commented May 21, 2018

Ok let's see, nice stuff!

@matthid matthid merged commit 4ee2ee3 into fsprojects:release/rc May 21, 2018
@kblohm kblohm deleted the handleUserCancel branch May 21, 2018 19:52
@kblohm
Copy link
Contributor Author

kblohm commented May 21, 2018

As a side note for the tests with expecto: have you looked at https://github.com/YoloDev/YoloDev.Expecto.TestSdk ? No idea how good that is, but some people seem to be using it for dotnet core.

@matthid
Copy link
Member

matthid commented May 21, 2018

Relevant to your process-tree comment:

https://github.com/dotnet/cli/issues/7426

@kblohm
Copy link
Contributor Author

kblohm commented May 22, 2018

This is what some of the dotnet people are using "solve" that.

@matthid
Copy link
Member

matthid commented May 22, 2018

@kblohm Looks interesting, we might want to "save" that in a new issue. But as long as we have no reports about it ;)

@0x53A
Copy link
Contributor

0x53A commented May 22, 2018

But as long as we have no reports about it ;)

cough #1427

This would be a nice feature to add and a PR is very welcome. Closing this for now.

But yeah, I never did send a PR

@0x53A
Copy link
Contributor

0x53A commented May 22, 2018

Theoretically, if someone might want to start on this, what would the design be?

  1. kill all trees
  2. only kill trees with opt-in from user (would require adding a parameter to everything that spawns a process)
  3. kill everything, except opt-out from user, would also require adding this parameter

@matthid
Copy link
Member

matthid commented May 22, 2018

Yes or we just change the default behavior and don't make it opt-out either. Actually the process killing is already opt-out afaik

@matthid
Copy link
Member

matthid commented May 22, 2018

As in the old issue: PR is welcome ;)

@0x53A
Copy link
Contributor

0x53A commented May 22, 2018

so 1)? Kill all children started by FAKE?

@matthid
Copy link
Member

matthid commented May 22, 2018

Yes I don't see how that is worse than killing everything (ie what we already do)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants