packages feed

extensible-effects-concurrent-2.0.0: test/WatchdogTests.hs

{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoOverloadedStrings #-}

module WatchdogTests
  ( test_watchdogTests,
  )
where

import Common hiding (runTestCase)
import qualified Common
import Control.Eff.Concurrent.Protocol.Broker (Broker)
import qualified Control.Eff.Concurrent.Protocol.Broker as Broker
import qualified Control.Eff.Concurrent.Protocol.Observer.Queue as OQ
import Control.Eff.Concurrent.Protocol.StatefulServer (Stateful)
import qualified Control.Eff.Concurrent.Protocol.StatefulServer as Stateful
import qualified Control.Eff.Concurrent.Protocol.Watchdog as Watchdog
import Control.Lens
import qualified Data.Map as Map
import Data.Maybe (fromJust, isNothing)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String

runTestCase :: TestName -> Eff Effects () -> TestTree
runTestCase = Common.runTestCase

test_watchdogTests :: HasCallStack => TestTree
test_watchdogTests =
  testGroup
    "watchdog"
    [ runTestCase "demonstrate Bookshelf" bookshelfDemo,
      testGroup
        "watchdog broker interaction"
        [ runTestCase "test 1: when the broker exits, the watchdog does not care, it can be re-attached to a new broker" $ do
            wd <- Watchdog.startLink def
            unlinkProcess (wd ^. fromEndpoint)
            do
              broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
              unlinkProcess (broker ^. fromEndpoint)
              logNotice "started broker"
              Watchdog.attachTemporary wd broker
              logNotice "attached watchdog"
              assertShutdown (broker ^. fromEndpoint) ExitNormally
              logNotice "stopped broker"
            do
              broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
              unlinkProcess (broker ^. fromEndpoint)
              logNotice "started new broker"
              Watchdog.attachTemporary wd broker
              logNotice "attached broker"
              let expected = ExitUnhandledError (fromString "test-broker-kill")
              logNotice "crashing broker"
              assertShutdown (broker ^. fromEndpoint) expected
              logNotice "crashed broker"
            logNotice "shutting down watchdog"
            assertShutdown (wd ^. fromEndpoint) ExitNormally
            logNotice "watchdog down",
          runTestCase "test 2: when the broker exits, and the watchdog is linked to it, it will exit" $ do
            wd <- Watchdog.startLink def
            unlinkProcess (wd ^. fromEndpoint)
            logNotice "started watchdog"
            do
              broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
              unlinkProcess (broker ^. fromEndpoint)
              logNotice "started broker"
              Watchdog.attachPermanent wd broker
              logNotice "attached and linked broker"
              logNotice "crashing broker"
              let expected = ExitUnhandledError (fromString "test-broker-kill")
              sendShutdown (broker ^. fromEndpoint) expected
              logNotice "crashed broker"
              logNotice "waiting for watchdog to crash"
              awaitProcessDown (wd ^. fromEndpoint)
                >>= lift
                  . assertEqual "bad exit reason" (ExitOtherProcessNotRunning (broker ^. fromEndpoint))
                  . downReason
              logNotice "watchdog crashed",
          runTestCase "test 3: when the same broker is attached twice, the second attachment is ignored" $ do
            wd <- Watchdog.startLink def
            unlinkProcess (wd ^. fromEndpoint)
            logNotice "started watchdog"
            do
              broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
              unlinkProcess (broker ^. fromEndpoint)
              logNotice "started broker"
              Watchdog.attachTemporary wd broker
              logNotice "attached broker"
              Watchdog.attachTemporary wd broker
              logNotice "attached broker"
              logNotice "restart child test begin"
              restartChildTest broker
              logNotice "restart child test finished",
          runTestCase
            "test 4: when the same broker is attached twice, the\
            \ first linked then not linked, the watchdog is not linked anymore"
            $ do
              wd <- Watchdog.startLink def
              unlinkProcess (wd ^. fromEndpoint)
              logNotice "started watchdog"
              do
                broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                unlinkProcess (broker ^. fromEndpoint)
                logNotice "started broker"
                Watchdog.attachPermanent wd broker
                logNotice "attached and linked broker"
                Watchdog.attachTemporary wd broker
                logNotice "attached broker"
                logNotice "run restart child test"
                restartChildTest broker
                logNotice "restart child test finished"
                logNotice "crashing broker"
                let expected = ExitUnhandledError (fromString "test-broker-kill")
                assertShutdown (broker ^. fromEndpoint) expected
                logNotice "crashed broker"
              do
                broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                unlinkProcess (broker ^. fromEndpoint)
                logNotice "started new broker"
                Watchdog.attachTemporary wd broker
                logNotice "attached new broker"
                logNotice "run restart child test again to show everything is ok"
                restartChildTest broker
                logNotice "restart child test finished",
          runTestCase
            "test 5: when the same broker is attached twice,\
            \ the first time not linked but then linked, the watchdog is linked \
            \ to that broker"
            $ do
              wd <- Watchdog.startLink def
              unlinkProcess (wd ^. fromEndpoint)
              mwd <- monitor (wd ^. fromEndpoint)
              logNotice "started watchdog"
              do
                broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                unlinkProcess (broker ^. fromEndpoint)
                logNotice "started broker"
                Watchdog.attachTemporary wd broker
                logNotice "attached broker"
                Watchdog.attachPermanent wd broker
                logNotice "attached and linked broker"
                logNotice "run restart child test"
                restartChildTest broker
                logNotice "restart child test finished"
                logNotice "crashing broker"
                let expected = ExitUnhandledError (fromString "test-broker-kill")
                assertShutdown (broker ^. fromEndpoint) expected
                logNotice "crashed broker"
                logNotice "waiting for watchdog to crash"
                receiveSelectedMessage (selectProcessDown mwd)
                  >>= lift
                    . assertEqual "bad exit reason" (ExitOtherProcessNotRunning (broker ^. fromEndpoint))
                    . downReason
                logNotice "watchdog down",
          runTestCase
            "test 6: when multiple brokers are attached to a watchdog,\
            \ and a linked broker exits, the watchdog should exit"
            $ do
              wd <- Watchdog.startLink def
              unlinkProcess (wd ^. fromEndpoint)
              wdMon <- monitor (wd ^. fromEndpoint)
              logNotice "started watchdog"
              do
                broker1 <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                unlinkProcess (broker1 ^. fromEndpoint)
                logNotice "started broker 1"
                Watchdog.attachPermanent wd broker1
                logNotice "attached + linked broker 1"
                broker2 <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                logNotice "started broker 2"
                Watchdog.attachTemporary wd broker2
                logNotice "attached broker 2"
                broker3 <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                logNotice "started broker 3"
                Watchdog.attachTemporary wd broker3
                logNotice "attached broker 3"
                unlinkProcess (broker3 ^. fromEndpoint)
                let expected = ExitUnhandledError (fromString "test-broker-kill 1")
                logNotice "crashing broker 1"
                assertShutdown (broker1 ^. fromEndpoint) expected
                logNotice "crashed broker 1"
                isProcessAlive (broker2 ^. fromEndpoint)
                  >>= lift . assertBool "broker2 should be running"
                isProcessAlive (broker3 ^. fromEndpoint)
                  >>= lift . assertBool "broker3 should be running"
                logNotice "other brokers are alive"
                logNotice "waiting for watchdog to crash"
                receiveSelectedMessage (selectProcessDown wdMon)
                  >>= lift
                    . assertEqual "bad exit reason" (ExitOtherProcessNotRunning (broker1 ^. fromEndpoint))
                    . downReason
                logNotice "watchdog crashed"
        ],
      testGroup
        "restarting children"
        [ runTestCase "test 7: when a child exits it is restarted" $ do
            broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
            logNotice "started broker"
            wd <- Watchdog.startLink def
            logNotice "started watchdog"
            Watchdog.attachTemporary wd broker
            logNotice "attached broker"
            logNotice "running child tests"
            restartChildTest broker
            logNotice "finished child tests",
          runTestCase
            "test 8: when the broker emits the shutting down\
            \ event the watchdog does not restart children"
            $ do
              broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
              unlinkProcess (broker ^. fromEndpoint)
              logNotice "started broker"
              wd <- Watchdog.startLink def
              unlinkProcess (wd ^. fromEndpoint)
              logNotice "started watchdog"
              Watchdog.attachTemporary wd broker
              OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
                let c0 = BookShelfId 0
                do
                  void $ Broker.spawnOrLookup broker c0
                  awaitChildStartedEvent c0
                  logNotice "bookshelf started"
                  Broker.stopBroker broker
                  awaitBrokershuttingDownEvent broker
                  lift (threadDelay 10_000)
                  logNotice "checking state"
                  Watchdog.getCrashReports wd >>= lift . assertBool "no crash reports expected" . null
              assertShutdown (wd ^. fromEndpoint) ExitNormally
              logNotice "watchdog stopped",
          runTestCase "test 9: a child is only restarted if it crashes" $ do
            broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
            logNotice "started broker"
            wd <- Watchdog.startLink def
            logNotice "started watchdog"
            Watchdog.attachTemporary wd broker
            OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
              let shelfId = BookShelfId 0
              do
                shelf <- Broker.spawnOrLookup broker shelfId
                awaitChildStartedEvent shelfId
                logNotice "bookshelf started"
                cast shelf StopShelf
                awaitChildDownEvent shelfId
                void $ awaitProcessDown (shelf ^. fromEndpoint)
                logNotice "shelf stopped"
                lift (threadDelay 10_000)
                Broker.lookupChild broker shelfId >>= lift . assertBool "child must not be restarted" . isNothing
            assertShutdown (wd ^. fromEndpoint) ExitNormally
            logNotice "watchdog stopped",
          testGroup "child restart" $
            let threeTimesASecond = 3 `Watchdog.crashesPerSeconds` 1
             in [ runTestCase
                    "test 10: if a child crashes 3 times in 300ms, waits 1.1 seconds\
                    \ and crashes again 3 times, and is restarted three times"
                    $ do
                      broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      logNotice "started broker"
                      wd <- Watchdog.startLink threeTimesASecond
                      logNotice "started watchdog"
                      Watchdog.attachTemporary wd broker
                      OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
                        let crash3Times = do
                              logNotice "crashing 3 times"
                              replicateM_ 3 $ do
                                spawnAndCrashBookShelf broker shelfId
                                logNotice "waiting for last restart"
                                awaitChildStartedEvent shelfId
                                lift (threadDelay 100_000)
                            shelfId = BookShelfId 0
                        crash3Times
                        logNotice "sleeping"
                        lift (threadDelay 1_100_000)
                        logNotice "crashing again"
                        crash3Times
                      assertShutdown (wd ^. fromEndpoint) ExitNormally
                      logNotice "watchdog stopped",
                  runTestCase
                    "test 11: if a child crashes 4 times within 1s it is not restarted\
                    \ and the watchdog exits with an error"
                    $ do
                      broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      logNotice "started broker"
                      wd <- Watchdog.startLink threeTimesASecond
                      logNotice "started watchdog"
                      unlinkProcess (wd ^. fromEndpoint)
                      Watchdog.attachTemporary wd broker
                      OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
                        let shelfId = BookShelfId 0
                            crash3Times = do
                              logNotice "crashing 3 times"
                              replicateM_ 3 $ do
                                spawnAndCrashBookShelf broker shelfId
                                logNotice "waiting for restart"
                                awaitChildStartedEvent shelfId
                                cr <- Watchdog.getCrashReports @(Stateful BookShelf) wd
                                traverse_ (logNotice . LABEL "crash-reports") cr
                                lift (threadDelay 100_000)
                        crash3Times
                        logNotice "crashing 4. time"
                        spawnAndCrashBookShelf broker shelfId
                        lift (threadDelay 10_000)
                        Broker.lookupChild broker shelfId
                          >>= lift . assertBool "child must not be reststarted" . isNothing
                      assertShutdown (wd ^. fromEndpoint) ExitNormally
                      logNotice "watchdog stopped",
                  runTestCase
                    "test 12: if a child of a linked broker crashes too often,\
                    \ the watchdog exits with an error and interrupts the broker"
                    $ do
                      broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      logNotice "started broker"
                      unlinkProcess (broker ^. fromEndpoint)
                      mBroker <- monitor (broker ^. fromEndpoint)
                      wd <- Watchdog.startLink threeTimesASecond
                      logNotice "started watchdog"
                      mwd <- monitor (wd ^. fromEndpoint)
                      unlinkProcess (wd ^. fromEndpoint)
                      Watchdog.attachPermanent wd broker
                      OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
                        let shelfId = BookShelfId 0
                            crash3Times = do
                              logNotice "crashing 3 times"
                              replicateM_ 3 $ do
                                spawnAndCrashBookShelf broker shelfId
                                logNotice "waiting for restart"
                                awaitChildStartedEvent shelfId
                                lift (threadDelay 100_000)
                        crash3Times
                        logNotice "crashing 4. time"
                        spawnAndCrashBookShelf broker shelfId
                      logNotice "await broker down"
                      receiveSelectedMessage (selectProcessDown mBroker)
                        >>= lift
                          . assertEqual
                            "wrong exit reason"
                            (ExitUnhandledError (fromString "restart frequency exceeded"))
                          . downReason
                      logNotice "broker down"
                      logNotice "await watchdog down"
                      receiveSelectedMessage (selectProcessDown mwd)
                        >>= lift
                          . assertEqual
                            "wrong exit reason"
                            (ExitUnhandledError (fromString "restart frequency exceeded"))
                          . downReason
                      logNotice "watchdog down",
                  runTestCase
                    "test 13: if the watchdog is killed, all watched children of all\
                    \ brokers are still alive"
                    $ do
                      wd <- Watchdog.startLink (1 `Watchdog.crashesPerSeconds` 1)
                      unlinkProcess (wd ^. fromEndpoint)
                      logNotice "started watchdog"
                      brokerT <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      unlinkProcess (brokerT ^. fromEndpoint)
                      logNotice "started temporary broker"
                      brokerP <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      unlinkProcess (brokerP ^. fromEndpoint)
                      logNotice "started permanent broker"
                      Watchdog.attachTemporary wd brokerT
                      logNotice "attached temporary broker"
                      Watchdog.attachPermanent wd brokerP
                      logNotice "attached permanent broker"
                      let b0 = BookShelfId 0
                          b1 = BookShelfId 1
                          b2 = BookShelfId 2
                          b3 = BookShelfId 3
                      (e0, e1) <-
                        OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerT $
                          (,) <$> spawnBookShelf brokerT b0 <*> spawnBookShelf brokerT b1
                      m0 <- monitor (e0 ^. fromEndpoint)
                      m1 <- monitor (e1 ^. fromEndpoint)
                      logNotice (LABEL "started bookshelfs of temporary broker" (m0, m1))
                      (e2, e3) <-
                        OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerP $
                          (,) <$> spawnBookShelf brokerP b2 <*> spawnBookShelf brokerP b3
                      m2 <- monitor (e2 ^. fromEndpoint)
                      m3 <- monitor (e3 ^. fromEndpoint)
                      logNotice (LABEL "started bookshelfs of permanent broker" (m2, m3))
                      logNotice "crashing the watchdog"
                      assertShutdown (wd ^. fromEndpoint) (ExitUnhandledError (fromString "watchdog test crash"))
                      logNotice "watchdog stopped"
                      call e0 (AddBook "Solaris")
                      logNotice "e0 still alive"
                      call e1 (AddBook "Solaris")
                      logNotice "e1 still alive"
                      call e2 (AddBook "Solaris")
                      logNotice "e2 still alive"
                      call e3 (AddBook "Solaris")
                      logNotice "e3 still alive"
                      assertShutdown (e0 ^. fromEndpoint) ExitNormally
                      assertShutdown (e1 ^. fromEndpoint) ExitNormally
                      assertShutdown (e2 ^. fromEndpoint) ExitNormally
                      assertShutdown (e3 ^. fromEndpoint) ExitNormally
                      assertShutdown (brokerT ^. fromEndpoint) ExitNormally
                      assertShutdown (brokerP ^. fromEndpoint) ExitNormally,
                  runTestCase
                    "test 14: if the watchdog receives OnChildDown\
                    \ for a known broker it behaves as if there\
                    \ was an OnChildSpawned for that child"
                    $ do
                      wd <- Watchdog.startLink (1 `Watchdog.crashesPerSeconds` 1)
                      logNotice "started watchdog"
                      brokerT <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      logNotice "started temporary broker"
                      OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerT $ do
                        let b0 = BookShelfId 0
                        e0 <- spawnBookShelf brokerT b0
                        logNotice (LABEL "started bookshelf of temporary broker" e0)
                        Watchdog.attachTemporary wd brokerT
                        logNotice "attached temporary broker"
                        logNotice "crash the bookshelf"
                        assertShutdown (e0 ^. fromEndpoint) (ExitUnhandledError (fromString "bookshelf test crash"))
                        awaitChildDownEvent b0
                        logNotice "bookshelf down"
                        awaitChildStartedEvent b0
                        logNotice "bookshelf restarted"
                        assertShutdown (wd ^. fromEndpoint) ExitNormally
                        assertShutdown (brokerT ^. fromEndpoint) ExitNormally,
                  runTestCase
                    "test 15: when a child exits normally,\
                    \ and a new child with the same ChildId\
                    \ crashes, the watchdog behaves as if\
                    \ the first child never existed"
                    $ do
                      wd <- Watchdog.startLink (1 `Watchdog.crashesPerSeconds` 10)
                      logNotice "started watchdog"
                      brokerT <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                      logNotice "started temporary broker"
                      OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerT $ do
                        let b0 = BookShelfId 0
                        e0 <- spawnBookShelf brokerT b0
                        logNotice (LABEL "started bookshelf of temporary broker" e0)
                        Watchdog.attachTemporary wd brokerT
                        logNotice "attached temporary broker"
                        logNotice "crash the bookshelf"
                        assertShutdown (e0 ^. fromEndpoint) (ExitUnhandledError (fromString "bookshelf test crash"))
                        awaitChildDownEvent b0
                        logNotice "bookshelf down"
                        awaitChildStartedEvent b0
                        e1 <- fromJust <$> Broker.lookupChild brokerT b0
                        logNotice "bookshelf restarted"
                        logNotice "exit the bookshelf normally"
                        assertShutdown (e1 ^. fromEndpoint) ExitNormally
                        awaitChildDownEvent b0
                        logNotice "bookshelf stopped, starting a new one"
                        e2 <- spawnBookShelf brokerT b0
                        logNotice "bookshelf restarted"
                        assertShutdown (e2 ^. fromEndpoint) (ExitUnhandledError (fromString "bookshelf test crash"))
                        awaitChildDownEvent b0
                        logNotice "bookshelf down"
                        awaitChildStartedEvent b0
                        logNotice "bookshelf restarted"
                        assertShutdown (wd ^. fromEndpoint) ExitNormally
                        assertShutdown (brokerT ^. fromEndpoint) ExitNormally,
                  testGroup "test 16: the amount of state is kept to a minimum" $
                    let setup k = do
                          wd <- Watchdog.startLink (4 `Watchdog.crashesPerSeconds` 30)
                          logNotice "started watchdog"
                          brokerT <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                          logNotice "started temporary broker"
                          Watchdog.attachTemporary wd brokerT
                          logNotice "attached temporary broker"
                          brokerP <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
                          logNotice "started permanent broker"
                          Watchdog.attachTemporary wd brokerP
                          logNotice "attached permanent broker"
                          do
                            p <- self
                            t <- spawn (fromString "test-proc") $ do
                              void $ k wd brokerT brokerP
                              sendMessage p ()
                            res <- receiveSelectedWithMonitorAfter t selectMessage (TimeoutMicros 10_000_000)
                            logNotice (LABEL "test-proc finish message" res)
                            case res of
                              Left x ->
                                do
                                  logError (LABEL "test-proc crashed" x)
                                  lift (assertFailure ("test-proc crashed: " <> showAsLogMsg x))
                              Right () ->
                                logNotice "test-proc succeeded"
                        setupThenShutdown k =
                          setup $ \wd brokerT brokerP -> do
                            void $ k wd brokerT brokerP
                            assertShutdown (wd ^. fromEndpoint) ExitNormally
                            assertShutdown (brokerT ^. fromEndpoint) ExitNormally
                            assertShutdown (brokerP ^. fromEndpoint) ExitNormally
                     in [ runTestCase "test 17: child events of unknown broker don't add state" $
                            setupThenShutdown $
                              \wd _brokerT _brokerP -> do
                                someBroker <- asEndpoint @(Broker (Stateful BookShelf)) <$> spawn (fromString "someBroker") (pure ())
                                someChild <- asEndpoint @BookShelf <$> spawn (fromString "someChild") (pure ())
                                let someChildId = BookShelfId 2321
                                cast wd (Observed (Broker.OnChildDown someBroker someChildId someChild (ExitUnhandledError (packLogMsg "test error"))))
                                lift (threadDelay 1_000)
                                cr <- Watchdog.getCrashReports @(Stateful BookShelf) wd
                                traverse_ (logNotice . LABEL "crash-reports") cr
                                lift (assertBool "no crash reports expected" (Map.null cr)),
                          runTestCase "test 18: when a broker exits, the children of that broker are forgotten and ignored" $ do
                            setup $ \wd brokerT brokerP -> do
                              let someChildId1 = BookShelfId 2321
                                  someChildId2 = BookShelfId 2322
                                  someChildId3 = BookShelfId 2323
                              (someChild1, someChild2) <-
                                OQ.observe @(Broker.ChildEvent (Stateful BookShelf))
                                  (100 :: Int)
                                  brokerT
                                  ((,) <$> spawnBookShelf brokerT someChildId1 <*> spawnBookShelf brokerT someChildId2)
                              someChild3 <-
                                OQ.observe @(Broker.ChildEvent (Stateful BookShelf))
                                  (100 :: Int)
                                  brokerP
                                  (spawnBookShelf brokerP someChildId3)
                              do
                                cr <- Watchdog.getCrashReports @(Stateful BookShelf) wd
                                traverse_ (logNotice . LABEL "crash-report") cr
                                lift (assertBool "no crash reports expected" (Map.null cr))
                              OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerT $ do
                                crashBookShelf someChild1 someChildId1
                                awaitChildStartedEvent someChildId1
                                crashBookShelf someChild2 someChildId2
                                awaitChildStartedEvent someChildId2
                              OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) brokerP $ do
                                crashBookShelf someChild3 someChildId3
                                awaitChildStartedEvent someChildId3
                              do
                                cr <- Watchdog.getCrashReports @(Stateful BookShelf) wd
                                traverse_ (logNotice . LABEL "crash-report") cr
                                lift (assertEqual "wrong number of crash reports" 3 (Map.size cr))
                              assertShutdown (brokerT ^. fromEndpoint) ExitNormally
                              do
                                cr <- Watchdog.getCrashReports @(Stateful BookShelf) wd
                                traverse_ (logNotice . LABEL "crash-report") cr
                                lift (assertEqual "wrong number of crash reports" 1 (Map.size cr))
                              assertShutdown (wd ^. fromEndpoint) ExitNormally
                              assertShutdown (brokerP ^. fromEndpoint) ExitNormally
                        ]
                ]
        ]
    ]

bookshelfDemo :: HasCallStack => Eff Effects ()
bookshelfDemo = do
  logNotice "Bookshelf Demo Begin"
  broker <- Broker.startLink (Broker.statefulChild @BookShelf (TimeoutMicros 1_000_000) id)
  shelf1 <- Broker.spawnOrLookup broker (BookShelfId 1)
  call shelf1 (AddBook "Solaris")
  call shelf1 GetBookList >>= mapM_ logDebug
  cast shelf1 (TakeBook "Solaris")
  call shelf1 GetBookList >>= mapM_ logDebug
  logNotice "Bookshelf Demo End"

restartChildTest :: HasCallStack => Endpoint (Broker (Stateful BookShelf)) -> Eff Effects ()
restartChildTest broker =
  OQ.observe @(Broker.ChildEvent (Stateful BookShelf)) (100 :: Int) broker $ do
    let c0 = BookShelfId 123
    spawnAndCrashBookShelf broker c0
    c01m <- Broker.lookupChild broker c0
    case c01m of
      Nothing ->
        lift (assertFailure "failed to lookup child, seems it wasn't restarted!")
      Just c01 -> do
        call c01 (AddBook "Solaris")
        logNotice "part 3 passed"

spawnBookShelf ::
  HasCallStack =>
  Endpoint (Broker (Stateful BookShelf)) ->
  Broker.ChildId (Stateful BookShelf) ->
  Eff (OQ.Reader (Broker.ChildEvent (Stateful BookShelf)) : Effects) (Endpoint BookShelf)
spawnBookShelf broker c0 = do
  logNotice "spawn or lookup book shelf"
  res <- Broker.spawnChild broker c0
  c00 <- case res of
    Left (Broker.AlreadyStarted _ ep) -> pure ep
    Right ep -> awaitChildStartedEvent c0 >> pure ep
  logNotice (LABEL "got book shelf" c00)
  pure c00

spawnAndCrashBookShelf ::
  HasCallStack =>
  Endpoint (Broker (Stateful BookShelf)) ->
  Broker.ChildId (Stateful BookShelf) ->
  Eff (OQ.Reader (Broker.ChildEvent (Stateful BookShelf)) : Effects) ()
spawnAndCrashBookShelf broker c0 = do
  c00 <- spawnBookShelf broker c0
  crashBookShelf c00 c0

crashBookShelf ::
  HasCallStack =>
  Endpoint BookShelf ->
  Broker.ChildId (Stateful BookShelf) ->
  Eff (OQ.Reader (Broker.ChildEvent (Stateful BookShelf)) : Effects) ()
crashBookShelf c00 c0 = do
  logNotice "adding Solaris"
  call c00 (AddBook "Solaris")
  logNotice "added Solaris"
  logNotice "adding Solaris again to crash the book shelf"
  handleInterrupts (const (pure ())) (call c00 (AddBook "Solaris")) -- adding twice the same book causes a crash
  logNotice "added Solaris again waiting for crash"
  void $ awaitProcessDown (c00 ^. fromEndpoint)
  logNotice "shelf crashed"
  awaitChildDownEvent c0

awaitBrokershuttingDownEvent broker = do
  logNotice (LABEL "waiting for broker shutting down event of" broker)
  evt <- OQ.await @(Broker.ChildEvent (Stateful BookShelf))
  case evt of
    x@(Broker.OnBrokerShuttingDown broker')
      | broker == broker' ->
        logNotice x
    otherEvent ->
      lift (assertFailure ("wrong event received: " <> showAsLogMsg otherEvent))

awaitChildDownEvent c0 = do
  logNotice (LABEL "waiting for down event of" c0)
  evt <- OQ.await @(Broker.ChildEvent (Stateful BookShelf))
  case evt of
    x@(Broker.OnChildDown _ c' _ _)
      | c0 == c' ->
        logNotice x
    otherEvent ->
      lift (assertFailure ("wrong broker down event received: " <> showAsLogMsg otherEvent))

awaitChildStartedEvent c0 = do
  logNotice (LABEL "waiting for start event of" c0)
  evt <- OQ.await @(Broker.ChildEvent (Stateful BookShelf))
  case evt of
    x@(Broker.OnChildSpawned _ c' _)
      | c0 == c' ->
        logNotice x
    otherEvent ->
      lift (assertFailure ("wrong broker start event received: " <> showAsLogMsg otherEvent))

data BookShelf deriving (Typeable)

type Book = String

instance HasPdu BookShelf where
  data Pdu BookShelf r where
    GetBookList :: Pdu BookShelf ('Synchronous [Book])
    AddBook :: Book -> Pdu BookShelf ('Synchronous ())
    TakeBook :: Book -> Pdu BookShelf 'Asynchronous
    StopShelf :: Pdu BookShelf 'Asynchronous
    deriving (Typeable)

instance Stateful.Server BookShelf Effects where
  newtype StartArgument BookShelf = BookShelfId Int
    deriving (Show, Eq, Ord, Num, NFData, Typeable)

  newtype Model BookShelf = BookShelfModel (Set String) deriving (Eq, Show, Default)

  update _ shelfId event = do
    logDebug shelfId event
    case event of
      Stateful.OnCast StopShelf -> do
        logNotice (MSG "stop shelf")
        exitNormally
      Stateful.OnCast (TakeBook book) -> do
        booksBefore <- Stateful.getModel @BookShelf
        booksAfter <- Stateful.modifyAndGetModel (over bookShelfModel (Set.delete book))
        when (booksBefore == booksAfter) (exitWithError "book not taken")
      Stateful.OnCall rt callEvent ->
        case callEvent of
          GetBookList -> do
            books <- Stateful.useModel bookShelfModel
            sendReply rt (toList books)
          AddBook book -> do
            booksBefore <- Stateful.getModel @BookShelf
            booksAfter <- Stateful.modifyAndGetModel (over bookShelfModel (Set.insert book))
            when (booksBefore == booksAfter) (exitWithError "book not added")
            sendReply rt ()
      other -> logWarning other

bookShelfModel :: Iso' (Stateful.Model BookShelf) (Set String)
bookShelfModel = iso (\(BookShelfModel s) -> s) BookShelfModel

instance ToLogMsg (Stateful.StartArgument BookShelf) where
  toLogMsg (BookShelfId bId) = packLogMsg "BookShelf" <> toLogMsg bId

type instance Broker.ChildId BookShelf = Stateful.StartArgument BookShelf

instance NFData (Pdu BookShelf r) where
  rnf GetBookList = ()
  rnf StopShelf = ()
  rnf (AddBook b) = rnf b
  rnf (TakeBook b) = rnf b

instance ToLogMsg (Pdu BookShelf r) where
  toLogMsg GetBookList = packLogMsg "get-book-list"
  toLogMsg StopShelf = packLogMsg "stop-shelf"
  toLogMsg (AddBook b) = packLogMsg "add-book: " <> toLogMsg b
  toLogMsg (TakeBook b) = packLogMsg "take-book: " <> toLogMsg b

instance ToTypeLogMsg BookShelf where
  toTypeLogMsg _ = fromString "BookShelf"