Closed tonicebrian closed 1 year ago
What does the spawn function do?
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"
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.
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
What I'm trying to figure out is where is this MVar
operation which GHC seems to be referencing. We don't use any MVar
s in my code, so it must be in yours?
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.
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!
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!!
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.
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:
blocks the program with this result:
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?