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

Move Giraffe.Tasks into separate NuGet package? #81

Closed
dustinmoris opened this issue Aug 14, 2017 · 36 comments
Closed

Move Giraffe.Tasks into separate NuGet package? #81

dustinmoris opened this issue Aug 14, 2017 · 36 comments

Comments

@dustinmoris
Copy link
Member

Since there is a lot of lower level libraries which then would get used higher up from a Giraffe web application they will probably need/want to expose endpoints which return a Task as well instead of an Async if there is some asynchronous work to be done.

In this instance it might make more sense to only have to reference a smaller NuGet package which only exposes the task {} workflow rather than having to reference the entire Giraffe web framework.

I think this will become a common use case, so should think about it?

What do people think?

/cc @gerardtoconnor

@JonCanning
Copy link
Contributor

JonCanning commented Aug 14, 2017 via email

@kspeakman
Copy link

kspeakman commented Aug 14, 2017

TLDR: yes, separate package.

I downloaded beta1 today (not on purpose, NuGet restore did it) and could not build until I rewrote my routes and helpers to use Tasks. This brought me to 3 conclusions.

  1. The primary reason I want to use Giraffe is to have an idiomatic F# API for specifying routes.
  2. Task is not a built-in idiomatic F# pattern.
  3. Async performance is good enough for my usage -- especially considering my previous tech was Web API 2 (order of magnitude slower than async Giraffe).

I think the Task API will be useful for heavy C# interop (and therefore good as a separate package), but it is not my preferred default for Giraffe.

Edit: Some usage info. I'm using only for API endpoint, with HTML5 front-end (served separately from API).

@dustinmoris
Copy link
Member Author

dustinmoris commented Aug 14, 2017

@kspeakman Good feedback, thank you. I am not sure if I understood you 100% correctly, but given that the HttpHandler has changed to return a Task<..> now is this an issue for you? In theory not much has changed, because ASP.NET Core was always task only, the only difference is that we moved that requirement one level higher up to Giraffe now, but your internal code can still work with F# Async and then convert back to a Task at the Giraffe level. Is that an acceptable use case or does that cause any problems?

@gerardtoconnor
Copy link
Member

The Task CE is just syntactic sugar for binding Task continuations, and TPL Tasks are a BCL for .net so any low-level libraries one would use would already have their own methods for handling Tasks.

When building a Giraffe app, package dependencies will be mostly Nuget, so I presume you are talking about a custom personal project dependencies (lib) that are to be used from within a separate Giraffe app? In this case, it might be useful to allow the external use of task CE although is it painful to maintain another separate nuget package?

@dustinmoris May be worth adding AutoOpenAttribute to task file so that users do not have to open Giraffe.Tasks explicitly every time as it is so common?

@JonCanning As mentioned above, it is mainly for binding tasks but there are specific functionality that will be built in specifically for Giraffe (Context-Insensitive, Async Overloading) but functionality that doesn't depend on Giraffe and can be used without it.

@kspeakman Unfortunately, without HKT or Type Classes, we are not able to maintain both an async {} & task {} version of Giraffe as it is used explicitly throughout. Giraffe tries to be idomatic with both asp.net core as much as it is idomatic with f#; being a light functional wrapper (with helper extensions) around asp.net core; so it makes sense to use tasks throughout. I am hoping to update task CE with async overload shortly so that you can use async with task {}, hope this will make the transition easier. Async performance is not good enough for some people's usage and no-one ever complains about things being too fast so we need to focus on performance case but appreciate where you're coming from.

@kspeakman
Copy link

(separate issue) The Giraffe API change with beta1 was breaking from the previous version (alpha 025), but was versioned in such a way that NuGet thought it was a minor (non-breaking) change. So just doing a restore broke my code, which was unexpected.

With Web API 2, I was used to returning Tasks from my code anyway. But one of the reasons I was drawn to initially to Suave was because I could use idiomatic F# all the way to the edge (of my code... I know Suave had to convert to Task at some point). And then I came to Giraffe because I could do the same on top of aspnet stack. So change to Task is not a problem for me, per se. Just highlighting that it seems like a goal change toward performance at expense of API.

Additionally, not sure if Tasks can be tail call optimized as Asyncs are. See last section of this README.

@kspeakman
Copy link

kspeakman commented Aug 14, 2017

Async performance is not good enough for some people's usage

Just curious. What are the performance-oriented use cases you are targeting?

@gerardtoconnor
Copy link
Member

gerardtoconnor commented Aug 14, 2017

Additionally, not sure if Tasks can be tail call optimized as Asyncs are. See last section of this README.

This is not an issue in our implementation as we are not doing recursive calls on task continueWith, the bind is executed as progressed. The TCO from using the continuations in the handler is referring to the stack frames built up from piped functions (stack frames from few async operations acceptable).

@gerardtoconnor
Copy link
Member

Just curious. What are the performance-oriented use cases you are targeting?

I'll get back to you on this one ...

Just highlighting that it seems like a goal change toward performance at expense of API.

I know everyone wants to keep the API as simple as possible but after lengthy discussion, we thought that this was a minimal impact on API while greatly improving performance. is it the continuation format or task computation expression that you feel is complicating API?

@kspeakman
Copy link

kspeakman commented Aug 15, 2017

I must admit I did find the HttpFunc -> HttpFunc definition more confusing than the previous API. However, it is similar in concept to ASP.NET's own Func<RequestDelegate, RequestDelegate> definition for middleware. I understand CEs pretty well. But task is a new one, whereas most F# users will already be using async and familiar with its characteristics.

I think the performance improvement of going Task-based is impressive. (I saw the numbers on TheAngryByrd's benchmark project.) However, it is not a primary motivator for me. In the larger picture, it is our least-concerning bottleneck (with database being first), and the easiest one to scale (since our web services are stateless, just add more nodes behind a load balancer).

@gerardtoconnor
Copy link
Member

The signature is not as descriptive as before I agree, I prefer to look at it as (Continuation & Context) -> Result, the middleware signature is exactly how we're using it as you noticed.

If databases are your issue then it may be worth looking into what kind of local caching you can do to reduce DB runs, as Giraffe is using asp.net core and you can inject different types of caching services into the context, hopefully, you can alleviate bottleneck but as you know, persistence concurrency is whole other can of worms we best not get into!

@kspeakman
Copy link

kspeakman commented Aug 15, 2017

We aren't having perf issues with the database currently. Just saying from a holistic perspective, the potential ops of most databases including ours is far lower than even async Giraffe.

@NinoFloris
Copy link
Member

@gerardtoconnor @dustinmoris Thanks for switching to Task, this was a blocker for us to reach good enough performance and so we migrated to asp.net 2.0 core without a nice functional layer in between. https://twitter.com/NinoFloris/status/892478914345541632

Definitely going to revisit this soon (Giraffe needs a 2.0 migration/branch though, would you mind if I submit a PR for that?)

@dustinmoris
Copy link
Member Author

@NinoFloris Great to hear! Yes a PR for the 2.0 migration would be perfect. I was going to have a crack at it myself over the weekend if nobody else would have provided something till then :). I know that @Banashek was having a go in #76 but I don't know what the latest status is today.

@ian-moore
Copy link

I agree with @kspeakman - it was nice having a data/domain layer that could be idiomatic F# with async that operated seamlessly with Giraffe handlers. Now I'm in a spot where I have to rewrite my entire app to use Tasks, or constantly pass one back to the other, which feels like pollution.

@kspeakman
Copy link

@ian-moore It's actually not too bad to setup custom middleware to implement async-based again. See this gist. Should be able to copy most of the HttpHandlers from alpha-025. I put only limited ones in the gist (in Combinators.fs) and tweaked them slightly.

@ian-moore
Copy link

@kspeakman thanks for that gist. I would hate to ditch Giraffe but if I can get what I want out of 2 simple modules I may give it a shot.

@dustinmoris
Copy link
Member Author

dustinmoris commented Aug 19, 2017

@kspeakman @ian-moore Hey guys, thanks for your feedback. I understand your motivation. The plan is however that we will make it possible to use async workflows from within the task {} CE without any additional conversions, which would mean that apart that the the signature of a HttpHandler has changed to Task instead of Async you shouldn't notice any difference from before. Would this address your current issue sufficiently? You'll essentially be able to stay completely Fsharpy with Async throughout your entire codebase and on the top layer in Giraffe use it natively from within a HttpHandler which has task {}.

@gerardtoconnor I think before we make any more perf improvements we should make this a priority to further reduce any friction for existing Giraffe users.

@gerardtoconnor
Copy link
Member

@kspeakman @ian-moore @dustinmoris I will try do PR this morning to implement Async binding overload as a priority so people can use Async methods in the task {} natively. As before, any async used would be a duplication of scheduling so task methods should be used as a preference where possible.

@gerardtoconnor
Copy link
Member

PR now added #84

HttpHandler will still return Task<'T> but inside any task { }, you can now bind async methods/operations into the pipeline.

I hope this mitigates some of the reservations @kspeakman & @ian-moore have in using the new TaskBuilder?

@dustinmoris
Copy link
Member Author

@gerardtoconnor Excellent, thank you. This was extremely fast!

@dustinmoris
Copy link
Member Author

dustinmoris commented Aug 19, 2017

Ok, so Giraffe 0.1.0-beta-002 has been released and hopefully resolves the issues you had. That still leaves me with the question if we think it would be beneficial to have the tasks exposed through a small NuGet package which can be used in custom projects which are built for Giraffe or leave it the way it is now?

@gerardtoconnor
Copy link
Member

I am happy with either, can see the benefit of splitting out purely for the development of giraffe compatible libraries to be referenced by a giraffe app, and I also like the simplicity of just leaving embedded but laziness aside, should probably be split out into a separate NuGet package.

If any of the NuGet pros in here would like to assist, would be greatly appreciated, just a simple case of moving Tasks.fs to Giraffe.Tasks package (he says not knowing how to do himself ¯\_(ツ)_/¯ ).

@dustinmoris
Copy link
Member Author

Cool... yes this should be fairly simple and I can do it. I'll wait for a few more responses and then do something about it :)

@gerardtoconnor
Copy link
Member

Thanks for switching to Task, this was a blocker for us to reach good enough performance and so we migrated to asp.net 2.0 core without a nice functional layer in between.

@NinoFloris Giraffe has a focus on performance as well as being functionally idiomatic with asp.net core. We will be looking to have framework performance equal or better than MVC, but with a significantly easier functional API. Hopefully, we'll be .net code 2.0 shortly.

Now I'm in a spot where I have to rewrite my entire app to use Tasks, or constantly pass one back to the other, which feels like pollution.

@ian-moore We naturally didn't want to cause a major disruption to existing (&future) apps and I am wondering how are you architecting your application that it is so reliant in async? asp.net core and its services all use .net Task so can you perhaps share some code examples of async usage issues that have made Task problematic so we can assist and perhaps address for you & others going forward?

The Combinators.fs Gist is very similar to original Giraffe Alpha implementation but as discussed, has performance issues from binding, wrapping sync in async, and scheduling duplication from constant async <> task conversions. Out of curiosity, what is the primary motivation for using asp.net core if not for performance & use of its platform services/infrastructure?

@NinoFloris
Copy link
Member

NinoFloris commented Aug 19, 2017

So why not create a package of the original you've included? https://github.com/rspeele/TaskBuilder.fs
rspeele/TaskBuilder.fs#4

@kspeakman
Copy link

@gerardtoconnor Single node perf is an important advantage to asp.net core, but there are others. Another motivation for me is being able to run on linux boxes for lower overhead (i.e. compute/mem cost) and large ecosystem of supporting services. If I need more ops than what a single-node provides, I add more nodes since my services are stateless.

To really gain the single-node perf from using Tasks, I will have to use the task CE all the way down in my code. Otherwise I am still doing Async-to-Task switching. I can (and do) use async tail recursively for my own internal operations (e.g. MailboxProcessor), which I don't think I would be able to do with Tasks.

xx% better single-node perf makes it marginally cheaper per node to operate. But the dev cost of switching everything over to Tasks to actually reap that benefit is a far larger cost for my project. If I were already using Tasks everywhere, I could see it being worth it.

I'm not suggesting you change directions, just providing feedback. (Was in the middle of porting our apis to core when beta1 hit.) Based on @NinoFloris comments, this change obviously hits somebody's use case, just not mine. That's ok, no hard feelings here.

As for putting Giraffe.Task in its own NuGet. I would find it really odd to reference Giraffe.Task in, for example, my database library which takes no dependencies on web stuff. I think it would be better if the Task CE implementation was wholly separate from Giraffe, and Giraffe happened to use it, and so could consumers. Or they could use a different one if they chose.

@gerardtoconnor
Copy link
Member

So why not create a package of the original you've included?

@NinoFloris We need the flexibility to be able to modify going forward, we have already removed the context-sensitive code and added async overloading, and I will be tweaking performance in the near future to remove DU bindings so makes sense to have our own isolated version Giraffe controls.

To really gain the single-node perf from using Tasks, I will have to use the task CE all the way down in my code. Otherwise I am still doing Async-to-Task switching. I can (and do) use async tail recursively for my own internal operations (e.g. MailboxProcessor), which I don't think I would be able to do with Tasks.

@kspeakman I think you should be able to workaround this, only the root calling HttpHandler needs to return a task, you can very simply internally use async throughout such that there is just one switch in the root handler, you could even define your own async child handler to be used internally that can bind the old way, and only use continuation & task format in root HttpHandler. The changing to task format is not supposed to stop or break people using async, just be mindful of only binding async to tasks as infrequently as possible. As the vast majority of asp.net core api & services are Task, for most people it makes sense to use Task and apps already using large async workflows can bundle together in async {} and do a bind in final root HttpHandler.

"I add more nodes since my services are stateless." ... "I can (and do) use async for my own internal operations (e.g. MailboxProcessor)"

MailboxProcessors are for shared access/mutation of state (and they are bit slow at that)!?! There may be a better (context-insensitive) architecture alternative out there?

@kspeakman
Copy link

@gerardtoconnor That was the idea: that whatever web routing library I was using would take care of converting to tasks and still allow me to use idiomatic structures in my code. I'll check out your PR.

Good eye regarding MailboxProcessor vs statelessness. It was only an example that f# users might be using which would be incompatible with Task CE since it is recursive async by default. I use recursive async in a few places like processing bulk changes in a unit of work pattern. Should be able to refactor to fold, but not high on priority list.

@nojaf
Copy link
Contributor

nojaf commented Sep 14, 2017

Since it's only one file using paket there is no real need to have a separate nuget package.

Add github dustinmoris/Giraffe src/Giraffe/Tasks.fs to paket.dependencies and be done with it right?

@dustinmoris
Copy link
Member Author

I think I'll close this for now. If more people come and join to push for a separation then I can re-open again.

@draganjovanovic1
Copy link
Contributor

Any thoughts to use this library inside Giraffe? https://github.com/kekyo/FSharp.Control.FusionTasks

It is already available as a nuget package.

@dustinmoris
Copy link
Member Author

@draganjovanovic1 Hey I think we looked at this library, but I don't remember 100% anymore why we didn't end up using it but I'll have a closer look again and let you know what I think!

@dustinmoris
Copy link
Member Author

/cc @gerardtoconnor

@gerardtoconnor
Copy link
Member

@draganjovanovic1 Hi, thanks for looking out for this possible solution. The reason we cannot/should not use Fusion Tasks is that it overloads Async to accept Tasks, and return Async so the interop is great but all these Tasks are being rescheduled as Async with is where the performance hit comes. This is why we had to opt for a CE that stays as a Task, accepts Task & Async, but returns Task, overloading async requires rescheduling but given most operations with asp.net core ( HandlerDelegate, Reading & Writing) are all Task, means we have minimal waste in pipeline.

Many F# users are not concerned about perf and think that easy interop is the same as unified constructs as it reasonably makes no difference to them ... threading constructs, both Task&Async are expensive to use (allocation, GC, scheduling) and should only be used explicitly when needed for performance critical applications.

If I am wrong on my presumptions on FusionTasks please let me know as happy to look into it further with you, best performance without compromising simplicity/flexibility seems to be the goal and @dustinmoris very open to all input.

@draganjovanovic1
Copy link
Contributor

@gerardtoconnor, I understand your concern regarding the performance. I would say your presumptions are correct but will definitely dig deeper into the FusionTasks as I found it recently. BTW Having TaskBuilder as a separate nuget would be really great.

@kspeakman
Copy link

@gerardtoconnor @dustinmoris To follow up on this, I implemented my own idiomatic API on top of Kestrel -- like the old version of Giraffe -- for my own uses. I do not need that many features, so it was a tiny effort. However, I did eventually arrive at the conclusion that Task-based is probably the right approach, especially if you return ValueTask for combinators that just filter out requests based on HttpContext. You pay a pretty hefty cost for wrapping synchronous combinators in F#'s Async, whereas ValueTask is very low-cost for those cases. And most of the combinators I use could be synchronous right up to passing the execution off to my app logic. FWIW

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

No branches or pull requests

8 participants