question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Is there a recommended pattern for cancellation?

See original GitHub issue

When calling a HTTP API or doing other long-running tasks, one might need to support cancellation. This could be explicit using a “Cancel” button, or implicit, e.g. cancelling any ongoing requests when signing out of the app.

Is there a recommended pattern for cancellation in Fabulous? Should CancellationTokenSources for any unique cancellable request be stored in the model and replaced when cancelled, or are there better ways? (And would that cause issues if serializing/deserializing the whole model, including CancellationTokenSources?)

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:1
  • Comments:36 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
Ryan-Palmercommented, May 1, 2019

That was an interesting article. He makes some points which I am sure are valid on fully implemented actor based systems like Akka running at scale. I also totally agree that there is no point in using an actor if you don’t need to manage state, just use a normal function. I think his definition of cache is a bit different to mine, I just mean some data needed at runtime but not stored on disk, or an in-memory reflection of some on-disk data to make access easier and faster, not a static store.

I will start here by saying that I am a fairly junior programmer, about 5 years pro, and only a year on F#, and mostly self taught, so I am probably a little off the mark on some things here and welcome any input from more knowledgeable people.

I think it is initially important to make a distinction between using actors and the actor model of software design.

At a very basic level, an actor is just a recursive loop with a message queue that runs on its own thread - no more, no less. I really like the fact that F# has a simple, lightweight and stripped down implementation included out-of-the-box, the MailboxProcessor. As it just uses a delegate that runs recursively, you can replace the state on each loop, and because it has an inbox you can post it messages and it will work through them as and when it can.

The actor model isn’t a framework, it is a configuration-based model of computation and interaction, as opposed to a traditional state based turing machine model. Frameworks like Akka build on the concepts by providing tools such as an actor registry / resolution system, distribution across machines, failure handling, loads of stuff, but they aren’t the model themselves any more than Fabulous ‘is’ MVU.

If you are interested in the fundamental concepts and power of the model-proper, there is an essential talk by the guy who invented it, Carl Hewitt (interviewed by Eric Meijer) here: https://www.youtube.com/watch?v=7erJ1DV_Tlo They explain (in an informal way) the essential axioms – that actors can only talk to other actors, and messages can be received at any time but and at most once etc etc. and what this brings to reasoning about interaction. The bit that really struck a chord with me is that it models indeterminacy (as opposed to non-determinacy) in a real way, which you can’t do with direct method calls, and that in turn forces you to consider it in your system design. It makes you think in terms of domain events which can happen in any time in any order. You think about the interaction much more than the state.

There is also a great amount of info here: https://doc.akka.io/docs/akka/current/guide/actors-intro.html

There is obviously a lot of potential to model complex applications using a full, strict system like this, and arguably you would get more out of it the more you embrace it, much like MVU and FP in general.

However, most of this is way more than I need right now for a mobile app and certainly a whole world away from what I am suggesting in the context of this thread.

I think talking about the actor model is a bit like event sourcing – everyone has a different understanding (or lack thereof) of exactly what it is or how you do it or to what extent.

I have taken the same approach here as I did there – try to understand the fundamental concepts, then implement the bits I understand which are useful in a pragmatic way to improve my daily code, rather than try to go full bore into creating a pure event sourced or actor based application because it is the latest thing and making an expensive mistake, or alternatively ignore it altogether and keep on doing things the inefficient way I had always done them.

A little background: I create Xamarin apps at work, previously in C# exclusively. I received the Reactive Patterns w/ Actors book I linked to earlier as a gift, and found it fascinating although a little heavyweight as Akka is a full framework and overkill for my needs and Scala makes my eyes bleed. Most of the book however is just a general pattern reference, it isn’t that specific to the language or framework.

A little while later I found (fell in love with) F# and started using it for the core Xamarin work. I needed to manage some application state, and the mailbox processor was easy to use and worked really well for many things. It allowed me to make use of many of those patterns where appropriate whilst not marrying a large framework, everything I needed was right there in the language.

It also was a revolution when dealing with the database. I no longer need to worry about concurrent writes. I have a single mailbox which manages the write connection. No more locks or anything. My reads happen using separate actors – I currently have one per table as I have a generic implementation that I instantiate with a return message type. This is my own ad-hoc approach which fits my apps, and is a lot better than what I had before (locking a SQLite connection). I never need to worry about massive parallel access, as my apps don’t do that. If they did, I would research an appropriate solution for that scenario.

The one thing I did find on my last project was that without a formal way of connecting the mailboxes, it can be hard to follow the flow of control for anyone unfamiliar with the architecture in question. That is why I have embraced MVU – it gives a rigid backbone for the application which can call into side-processors as necessary, making it easy to navigate and reason about.

It may not be perfect, and it may not be strict MVU or Actor Model or FP or OO or whatever, but I know why I made every decision (and compromise), it works really well and I have basically no external dependencies. It is the best of all the worlds I know of done the best way I know how, which is all any of us can do hey. I am working on a proper project with all of this included right now which I am happy to share if anyone is interested, it will end up on Github eventually.

Ok, so all of that said, I will reiterate that I am just simply talking about using the mailbox processor as a state bucket in this context, the most basic way you could think of. No big computing model or framework to deal with, just a small recursive loop with a stashed thing in it. The most basic way you can maintain state without a mutable field, and arguably simpler to reason about.

How would I implement the cancellation loop using the cmd I posted previously? Something like this:

    type ApiRequest = ApiRequest
    
    type ApiMessage =
        | Run of ApiRequest
        | Cancel

    type ApiCallback =
        | Reponse of Result<string,exn>
        | Cancelled

    type UpdateMessage =
        | RunRequest of ApiRequest
        | CancelRequest
        | Running of unit
        | Cancelling of unit
        | ApiComplete of ApiCallback
        | ApiError of exn

    type Dispatch<'a> = 'a -> unit
    type ApiEndpointManager<'a> = MailboxProcessor<ApiMessage*Dispatch<'a>*AsyncReplyChannel<unit>>

    let apiEndpointManager
        apiClient = // partially applied during construction
        ApiEndpointManager.Start (fun inbox ->
            let rec innerLoop (cts : CancellationTokenSource) =
                async {
                
                    // Run the request and callback when done
                    let execute request dispatch =
                        async {
                            let! response = apiClient request
                            dispatch response
                        }

                    //Grab a message + dispatch and replychannel from the inbox. Dispatch already has return message precomposed in Cmd.exec.
                    let! message, dispatch, replyChannel = inbox.Receive()
             
                    // Cancel existing token and create a new one
                    cts.Cancel()
                    let newCts = new CancellationTokenSource()

                    // Switch on message and act appropriately
                    match message with
                    | ApiMessage.Run request ->
                        let op = execute request dispatch
                        Async.StartImmediate(op, newCts.Token)
                    | ApiMessage.Cancel -> ()

                    replyChannel.Reply() // Use this to reply with Running content

                    // Call self with new state
                    do! innerLoop newCts
                }

            // kick off the recursive loop
            innerLoop (new CancellationTokenSource()))


    let update
        (apiFunc : ApiEndpointManager<ApiCallback>)
        msg
        model =
        match msg with
        | UpdateMessage.RunRequest rq -> 
            let newCommand = Cmd.ofActorAsync apiFunc (Run rq) ApiComplete Running ApiError
            model, newCommand
        | UpdateMessage.CancelRequest ->
            let newCommand = Cmd.ofActorAsync apiFunc Cancel ApiComplete Cancelling ApiError
            model, newCommand
        | UpdateMessage.Running () -> model, Cmd.none
        | UpdateMessage.Cancelling () -> model, Cmd.none
        | UpdateMessage.ApiComplete callback -> model, Cmd.none
        | UpdateMessage.ApiError e -> model, Cmd.none

I have included the types for completeness, it should actually compile if you import the cmd from earlier in the thread.

As previously mentioned, this example is a ‘one request at a time’ set-up where each cancels the last, but you could easily expand it, and adapt it to handle any kind of state.

The important bit is how nice it looks in the update func now, and if you strip all the comments out of the mailbox it is only a few lines. You are completely in control of how you use it, that depends on the mailbox body, and the Cmd is very generic, just like OfAsync but with an extra callback message.

1reaction
cmeerencommented, May 2, 2019

Extremely interesting that Elm used a mailbox processor originally rather than the ring bus - my own MVU thing I am using is all using the Mailbox, so I am interested to know why they switched.

Elmish, not Elm. I have no idea what Elm uses. (Not MailboxProcessor of course, since that’s F# specific). You can read about the reasons for the switch in this PR and the three linked issues: https://github.com/elmish/elmish/pull/160

What you have described as the solution to a threaded and concurrent requesting problem is exactly what I thought - IMHO a far more complex setup with a mutable dictionary, or that string based approach which is very limited to this particular application, not state management in general.

5 more simple-to-understand lines added to the already simple function-based solution is IMHO not vastly more complex - I’d say it’s still very simple. Also, I’m trying to solve this particular case, not state management in general. As I said, I appreciate that actors/queues/etc. is a very powerful pattern and the right tool for the job in many contexts. 😃

Thanks for the conversation! I’ll make sure to investigate MailboxProcessor a bit more on my own time.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Recommended patterns for CancellationToken
Know when you've passed the point of no cancellation. Don't cancel if you've already incurred side-effects that your method isn't prepared to ......
Read more >
Is there a recommended pattern for cancellation? #380
I have an app that is very API-based, where there is no technical reason why there can't be multiple requests in flight at...
Read more >
A Deep Dive into C#'s CancellationToken | by Mitesh Shah
Recommended Patterns for Cancellation Tokens. I wish I knew a lot about cancellation tokens to recommend some patterns of my own. But until...
Read more >
How to Create a Cancellation Policy: Templates + Examples
Try these cancellation policy templates, tips, and examples to write a winning cancellation policy for your home service business.
Read more >
6 Cancellation Flow Examples To Help Reduce Customer ...
Best collection of cancellation flow examples. Get inspiration from top SaaS brands and reduce churn by implementing churn surveys right ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found