Writing tests to demonstrate actor re-entrance bugs

Writing a test that detects when re-entrance occurs probably wouldn't even be meaningful because if the design allows it, it's eventually going to occur. Maybe collecting statistics would be more meaningful (i.e. in 1,000,000 randomly generated game sessions, 100 encounter re-entrance). But even then, you have to define what "re-entrance" means exactly. If it means simultaneous execution of two separate synchronous slices of a single async actor function, you need to define exactly what the entrance and exit criteria are for each slice. That a debugger would show two stacks with a frame for the actor function isn't really meaningful. You would need to set a flag at the "beginning" and "end" of each slice which defines the entrance and exit:

actor World {
  ...

  private var occupancy = 0 {
    didSet { if occupancy > 1 { print("Re-entrance detected") } }
  }

  func doSomeWork(with entity: Entity) async {
    // Some synchronous work
    occupancy += 1
    await doSomethingAsync()
    occupancy -= 1
    // More synchronous work
  }
}

But if you have to introduce state to track this, the re-entrance you have defined isn't meaningful to your app, it's inserted just to be tested.

Presumably you're worried about this because the async functions modify state during each synchronous slice of the body and you're worried that something will go wrong if those modifications get interleaved. You can test for that, but again if you design the code to allow it, it's just a matter of time before it happens. If it's incorrect for that to happen you need to design the code to prevent it.

If you want an exercise to test every possible interleaving combination, you can make the doSomethingAsync call mockable in tests and mock it with a continuation, and have a test spawn a parallel Task to call the actor function and use two continuations to synchronize the test and the Task. But I would be concerned if this was necessary to understand the implications of interleaving. The fact you're even concerned about it begs the question of why the design allows it at all.

I would expect a game design to make the entities actors and the World a struct (where the "world" is defined to the static/immutable part of the game and "entities" are dynamic parts that interact and change state during the game session). Don't the entities need unique identifiers? Wouldn't copying an entity be a non-trivial spawn operation that creates a new game object? I would expect the concurrency protection to need to apply to each individual entity's state, but no synchronization required between two entity's states. Do you imagine re-entrance would still a problem in that kind of design?

4 Likes