Seeking for Async architecture advice

Hi,

I’m looking for an advice on using and communicating async jobs. I have a ClientWebSocket that listens to incoming events, and occasionally sends queries. I need different async jobs to handle the events/responses and a way to communicate between all of them.

One option is to use MailboxProcessor but it’s fairly low level and most examples ignores many aspects of errors and disposes. I feel very comfortable working with async jobs communicating using channels and select model from Rust and tokio. I looked for something similar in F# and I found Hopac, which seems to be very popular but possibly not developed anymore(?) - last commit was 4 years ago.

So, my main questions are:

  • Is Hopac still a valid solution?
  • Is there another option / architecture that I should look into?

Thanks

Haim

Perhaps GitHub - fsprojects/FSharp.Control.TaskSeq: A computation expression and module for seamless working with IAsyncEnumerable<'T> as if it is just another sequence + Channel<T>?

Hopac is by all means a valid, if overkill and, depending on your taste, exotic, solution :slight_smile:
You would still need to explicitly deal with Disposal. That’s just something Rust does better than any other language. For channels, at work, I ended up implementing a range of homegrown abstractions on top of Channel to idiomatically detect if T is disposable and supply default “item dropped” handler if a channel is configured to drop items on over-capacity (otherwise the T is moved to consumer and disposing is now its responsibility). Another concern that Channel mis-abstracts, depending on how you look at it, is whether writes have back-pressure and may block vs writes that always succeed (even if it results in the item being dropped). Last concern, I suppose, is write cancellation (on backpressure) via CancellationToken. WriteAsync on the Channel luckily handles it as is.

I have something half-ready saved, although it’s more focused on pooling and syntax and does not really deal with aforementioned concerns fully. For select, one way to handle it is to invert the relationship and have multiple jobs write into a single channel with a listener (which I find a little more aesthetically pleasing than dealing with it via Task.WhenAny).

If you are interested, I can throw something together that fits your goal more closely.

Generally, if whatever listens on ClientWebSocket does not need to explicitly synchronize with the rest of the jobs, it’s best to let it go do its job back-to-back or just fire off tasks rather than creating a single serialized chokepoint, but I don’t have a wider context, just something I noticed is a fairly common mistake (for example listening on RabbitMQ for completely independent, transient requests instead of handling them without a queue at all).

1 Like

Thank you very much for the detailed answer. My use case is pretty simple but does require communications between jobs:

  • One job to listen to events / responses from the web socket and pass it ordered as strings to a channel without back-pressure to be consumed by other job
  • Second job that listens to channel messages and send each in order to the websocket.
  • There will be a couple of jobs and channels for parsing JSON and handling different types of responses.

Basically it’s a tray-icon app that communicates with tiling window manager and reflect the changes in the system tray. In Rust I wrote an app with similar scope using tokio and while you’ll be right to say it’s overkill, it was pretty easy to implement :slight_smile:.

Thinking again on the problem, there isn’t much difference in scope if I use jobs/channels or MBPs for this kind of scope. In both cases I need to listen and react on exceptions and disposals.

Maybe I’ll try to continue using MBPs and see how it goes…

Thanks

You can take a look at GitHub - TheAngryByrd/FSharp.Control.WebSockets: FSharp.Control.WebSockets wraps dotnet WebSockets in FSharp friendly functions and has a ThreadSafe version., either using it take inspiration from it.

1 Like

Cool, didn’t know it existed, thanks.

Hopac? It is not maintained (edit: looks like), and apparently there’s no way to make it work in Windows desktop programs (target has the “-windows” in it, so WPF or WinForms), even if you do use server garbage collection. Fun to tinker with, but unfortunately I can’t use it in my business. Too much risk in several ways. My apps are heavy on state machines in servers and desktops, and having different ways to deal with it would be less optimal. It’s better to use .NET where it’s reasonably good, and not rely on too many of the less used libraries from NuGet. Hopac do have an impressive 1.4M downloads, but then it’s been around for quite a while.

I really wanted to use Hopac, but gave up when I found out I could not get it to work with desktop apps. Most of my state machine logic is there, dealing with real time updates and client side communications.

Hopac targets netstandard2.0, it’s target-agnostic by definition. UI threading aside, why would you not be able to use it in an arbitrary .NET application?

I’d love to be wrong about this. I really like Hopac.

I had an intensive debug session with ChatGPT in order to figure out what was wrong. What ChatGPT found out is a bit over my head, but it concluded firmly that the Hopac scheduler was not initialized when an application targeted “-windows”. It suggested I test this with a simple console application with and without “-windows” in the target, but I had already gotten second thoughts about the risk at that point.

I also searched the issues in the repo for information. I found this issue particularly interesting: SynchronizationContext support · Issue #24 · Hopac/Hopac · GitHub

I believe I found more information indicating Hopac in desktop is not possible, but my memory about this is fading. Sorry about that.