SamuelSchlesinger / stm-actor

An implementation of the actor model in Haskell using STM
MIT License
16 stars 3 forks source link

Question about Actor initialization #9

Closed tonicebrian closed 1 year ago

tonicebrian commented 1 year ago

Hi, the initial part of an actor's ActionT code is to spawn another actor as a child. The parent actor is a coordinator so it also receives messages from the just spawned child and that's the reason for sending the self in the spawn function. The first action of the child is to request information from the parent by sending him a message.

The case is that this code from the parent:

program :: ActionT MatchCmd MatchActorM VP
program  = do
  parent <- self
  gameActor <- liftIO $ spawn parent startState
  link gameActor
  go
  where
    go :: ActionT MatchCmd MatchActorM VP
    go = receive \case
           ...... All messages

blocks the program with this result:

backend-test: thread blocked indefinitely in an MVar operation

If I remove the link gameActor action it completes successfully to the end.

Is that code above valid ActionT code? Maybe the child is sending messages when the receive function is still not in place? How should I create actors that will maintain bidirectional communication? Do you think the problem could be in other parts of my codebase?

SamuelSchlesinger commented 1 year ago

What does the spawn function do?

tonicebrian commented 1 year ago

It just fires up the Actor, I think the problem comes from the wrapUpGame function I use in the actFinally. It is trying to send the game result to the parent when the parent is already dead right? The parent calls the Murder on the "childs" when he is already dead, right?

spawn :: Actor MatchCmd -> GameState -> IO (Actor GameCmd)
spawn parent st0 = do
  let env = GameEnv parent
      actor = hoistActionT (runGame env st0) playGame
  actFinally wrapUpGame actor
  where
    wrapUpGame :: Either SomeException VP -> IO ()
    wrapUpGame (Right vps) = atomically $ send parent (GOutput $ DeclareWinner vps)
    wrapUpGame (Left _) = putStrLn "Unrecoverable error running the game"
SamuelSchlesinger commented 1 year ago

Haven't forgotten this, just busy with work stuff. I think there's an interesting race condition at play, maybe a flaw with the actor model or a bug in your code, not sure yet. Can you write a (not) working, minimal example of the problem occurring? It will help me assess.

tonicebrian commented 1 year ago

Yes. If you remember I was writing a game server for playing boardgames. The general setup of this example is that I have a MatchActor orchestrating player interactions asking and receiving user input. So the MatchActor is dealing with "partial" moves, like, "piece X selected", "destination cell Y pointed", etc. When all the data for a completely defined move is gathered, a move is sent to the GameActor like "move piece X to cell Y". The GameActor is acting as a state holder of the game, and when a move has been processed it emits a request to the MatchActor asking something for some player. That's the reason for linking the GameActor to the MatchActor, the moment the MatchActor is dismissed for whatever reason, there shouldn't be a running GameActor anywhere.

My code is a bit complex to paste here, let me try to add some pseudo code of what would it look like for a chess game:

--- This is the input of the MatchActor. It can be either something from the game actor or something from the users' clients
data MatchCmd = 
   GInput GameInput 
   | UInput UserInput

data GameInput = AskMove Player

data UserInput = 
  PickedPiece Player Piece 
  SelectedCell Player Row Column

 -- This is the "output" of the MatchActor. Moves are sent to the game actor
data Move = Move Player Piece Row Column 

-- This is the program for the `GameActor` that keeps state
gameProgram :: ActionT Move GameActorM VP
gameProgram = receive \case 
   Move p piece c col ->
        processChessMove
        parentRef <- ask (parentLens)  -- I use a ReaderT monad where I store the parent `Actor msg` reference
        send parentRef (AskMove (swap p)) -- Request move to the other player

program :: ActionT MatchCmd MatchActorM VP
program  = do
-- This is the program for the match
program :: ActionT MatchCmd MatchActorM VP
program  = do
  parent <- self
  gameActor <- liftIO $ spawn parent startState --- I need to pass the MatchActor to the GameActor so it knows the inbox where to respond
  link gameActor
  go
  where
    go :: ActionT MatchCmd MatchActorM VP
    go = receive \case
         UInput cmd -> \case 
           PickedPiece p piece -> 
               -- Here I have code for dealing with half backed moves, aka interactions
               requestPlayerCellForPiece p  -- Ask the player where the selected piece wants to move
            SelectedCell p c col -> 
               -- We have all the information to compose a move 
               thePiece <- retrievePreciouslySelectedPiece
               send gameActor (Move p thePiece c col)
          GInput cmd -> \case 
             AskMove p -> 
                -- The game requests a move from player p, so the match actor initiates the protocol to first request a piece and then a destination. The client will respond with a PickedPiece message
                sendPieceRequestTo p

Let me know if this is enough or do you need more description/code

SamuelSchlesinger commented 1 year ago

What I'm trying to figure out is where is this MVar operation which GHC seems to be referencing. We don't use any MVars in my code, so it must be in yours?

tonicebrian commented 1 year ago

Yes, that MVar is mine and it is used in the Sydtest that plays random games against itself. I use it to collect the final result of the actor by providing an actFinally function to deal with the generated value of the actor. I'm copying the code of the test:


spec :: Spec
spec = describe "A game played by picking random actions" do
  le <- liftIO $ do
    handleScribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V1
    registerScribe "stdout" handleScribe defaultScribeSettings =<< initLogEnv "doe-test" "production"
  let loggingEnv = KatipEnv le mempty mempty
  flaky 1 $ it "can be played until the end" do
    let sc = FullCampaign
        seed = mkStdGen 0
    result <- newEmptyMVar
    let errorHandling = \case
          Right vps -> do
            putMVar result vps
          Left e -> do
            putMVar result mempty
            expectationFailure (show e) :: IO ()
    matchActor <- spawnMatchActor errorHandling seed loggingEnv sc
    germanClient <- mkClient GermanEmpire (player GermanEmpire matchActor)
    russianClient <- mkClient RussianEmpire (player RussianEmpire matchActor)
    francoBritishClient <- mkClient FrancoBritishEmpire (player FrancoBritishEmpire matchActor)
    austroHungarianClient <- mkClient AustroHungarianEmpire (player AustroHungarianEmpire matchActor)

    atomically $ send matchActor (PlayerConnection GermanEmpire germanClient)
    atomically $ send matchActor (PlayerConnection RussianEmpire russianClient)
    atomically $ send matchActor (PlayerConnection FrancoBritishEmpire francoBritishClient)
    atomically $ send matchActor (PlayerConnection AustroHungarianEmpire austroHungarianClient)

    -- Just wait to finish
    void $ takeMVar result
    1 `shouldBe` (1 :: Int)

Clients here are Actors that pick random actions requested by the game.

I think that if it doesn´t ring a bell on you, maybe there is something wrong in my code and I should debug further. If you think that linking actors the way I'm doing it has no problems I'll try to square the bug in the future.

SamuelSchlesinger commented 1 year ago

I mean, the same ways you can create a program that is unresponsive with threads you can do this with actors. I'd be happy to pair with you on this particular deadlock, if you ever wanted to!

tonicebrian commented 1 year ago

I was going to prepare that joint session and I think I discovered the problem. I come from Akka and there the default strategy was to stop the children if the parent stopped. So when I wrote:

  link gameActor

I was understanding: "Hey, I'm your parent and whenever something happens to me I will send you a LinkKill exception" . In my use case the game ends before the Actor that is orchestrating players, so that code was working in reverse, sending the kill to the parent.

I was able to solve the problem by doing:

program :: StdGen -> Scenarios -> ActionT MatchCmd MatchActorM VP
program seed sc = do
  parent <- self
  gameActorRef <- liftIO $ spawn parent startState
  liftIO . atomically $ linkSTM gameActorRef parent
  go gameActorRef

So now the killing order is explicit and the parent cleans up resources it created.

I don't know if from a usability point of view having a supervise method with the semantics reversed would make sense. But at least the explicit method call with linkSTM solved my problem.

Thanks for the help!!

SamuelSchlesinger commented 1 year ago

Sounds great! If you have an idea for a new combinator you can feel free to submit a PR for it, or you can let me know what its semantics should be and I can try to build it.