Writing tests to demonstrate actor re-entrance bugs

This makes a lot of sense to me.

In a way, moving the internal state of the simulation into its own isolation boundary is a "brute force" way of eliminating suspension points. And validating state across suspension points should be testable. Tedious probably because of the small 'unit' that a suspension point likely represents, but still doable. Especially combined with your first approach.

I do believe unit tests are the way to test for this behaviour. Either the invariant is broken and we can recover, so we should throw an error. And test for that error getting thrown in the specific situation. Or the invariant is something we can't recover from (fatalError?). And we can always test whether certain mutations of state lead to an invariant being broken. I'll have to experiment with this approach.

Would this lead to 100% assurance from tests? Probably not. But that holds for synchronous code as well. And some tests still provide more confidence than no tests at all.

1 Like

Going back to the concurrency vs parallelism (because the distinction is actually important), google search gives us:

  • wikipedia/Concurrent_computing (emphasis mine, I also separated paragraphs for easier readability):

    The concept of concurrent computing is frequently confused with the related but distinct concept of parallel computing,[3][4] although both can be described as "multiple processes executing during the same period of time".

    In parallel computing, execution occurs at the same physical instant: for example, on separate processors of a multi-processor machine, with the goal of speeding up computations—parallel computing is impossible on a (one-core) single processor, as only one computation can occur at any instant (during any single clock cycle).

    By contrast, concurrent computing consists of process lifetimes overlapping, but execution need not happen at the same instant.

  • stackoverflow.com/what-is-the-difference-between-concurrency-and-parallelism - the top answer (emphasis mine):

    Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn't necessarily mean they'll ever both be running at the same instant. For example, multitasking on a single-core machine.

    Parallelism is when tasks literally run at the same time, e.g., on a multicore processor.

Concurrency is not Parallelism by Rob Pike (the video that I linked before) also uses the same definitions. One may discard this talk as niche because it has only 130k views, but at the same time is it THE Rob Pike. The most relevant part of the talk starts at 1:40.

I would say that concurrency happens at design time. We split the problem into multiple independent entities and we make sure that those entities work correctly "by themselves". The solution to the whole problem is a composition of those smaller things.

Parallelism is simultaneous execution.

In Swift we only deal with concurrency: we write code, we design stuff. Actor re-entrancy is 100% concurrency problem. If you have this problem then your design (code) is not correct. You can have re-entrance problem (or more generall: concurrency problem) regardless of whether you have parallel execution or not.

Ordering is yet something different. It is more like a single manifestation of multiple ways the program can execute. From the concurrency pov (which is what we are interested in when writing Swift code): if you design is correct it should handle any order without arriving into undesired/inconsistent state.

TLDR: when writing Swift code we only deal with concurrency not parallelism.

In a way parallelism/ordering do not matter, because we can’t control them anyway. If your design (the ā€œconcurrencyā€ part) is correct it will withstand everything.

Focusing on ā€œorderingā€ as a problem is a bit too specific, because there are many other things that can go wrong.

Ordering:

  • User liked post -> user disliked post
  • User disliked post -> user liked post

Obviously the 2nd one doesn't make any sense, but we still have to deal with it. Do we ignore the ā€œdislikeā€? Or do we have negative ā€œlikesā€ that will be balanced by a future ā€œlikeā€?

But at the same time we can also have:

  • User liked post -> user liked post -> user liked post

This is not really an ā€œorderingā€ problem. It is the same action repeated 3 times, the order does not matter. What is the result? 3 separate rows in database? ā€œUnique constraint violation (userId, postId)ā€? What if the user dislikes the post after? Does it cancel only 1 like or all 3 of them? Note that the order is fixed: 3 likes, followed by dislike.

Possible designs that can solve this ā€œrepeated likesā€ problem:

  • idempotence - remember only the 1st like
  • introducing a relationship between like/dislike, so that we know which ā€œlikeā€ was ā€œdislikedā€. For example the iOS one is cancelled, but the one from the web stays.

But this is purely a design problem, and it has nothing to do with parallelism or ordering. We just need to embrace the fact that outside of our tiny world we don’t control anything. As in: everything can happen.

TLDR; Concurrency is about the design, not about the things happening at the same time (which may not even happen). And ordering is just one of the problems.

So, how do you KNOW if you have this problem? This entire topic started on the assumption: "I am not sure if my design/code is having a problem. Can I use tests to gain more confidence?".

This I believe is the issue we need to solve: in a concurrent world, how can we be sure our designs are correct AND how can we be sure our code reflects the design correctly? I believe the topic brings some solutions, but we're not there yet.

2 Likes

That is an open question I guess — how to design? We still not sure what patterns and strategies can be used at large scale. Like there are a few that has been around for a while already, and we have to re-think sendability with RBI (for the good) currently, and many other unclear parts still out there. Getting some collection of best practices and recipes, along with some theory is still missing, as a significant part is scattered across proposals mostly, and you still have to figure out on your own how to use these.