{-# LANGUAGE GADTs #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE OverloadedStrings #-} {-| Module : Mealstrom.FSMApi Description : API for FSMs Copyright : (c) Max Amanshauser, 2016 License : MIT Maintainer : max@lambdalifting.org This is the interface through which you primarily interact with a FSM from the rest of your program. -} module Mealstrom.FSMApi where import Control.Concurrent import Control.Exception import Control.Monad (void) import qualified Data.Text as Text import System.IO import System.Timeout import Mealstrom.FSM import Mealstrom.FSMEngine import Mealstrom.FSMStore import Mealstrom.FSMTable import Mealstrom.WALStore data FSMHandle st wal k s e a where FSMHandle :: (Eq s, Eq e, Eq a, FSMStore st k s e a, WALStore wal k, FSMKey k) => { fsmStore :: st, -- ^ Which backend to use for storing FSMs. walStore :: wal, -- ^ Which backend to use for the WAL. fsmTable :: FSMTable s e a, -- ^ A table of transitions and effects. -- This is not in a typeclass, because you may want to use MVars or similar in effects. -- See the tests for examples. effTimeout :: Int, -- ^ How much time to allow for Actions until they are considered failed. retryCount :: Int -- ^ How often to automatically retry actions. } -> FSMHandle st wal k s e a get :: forall st wal k s e a . FSMStore st k s e a => FSMHandle st wal k s e a -> k -> IO(Maybe s) get FSMHandle{..} k = fsmRead fsmStore k (Proxy :: Proxy k s e a) -- |Idempotent because of usage of caller-generated keys. post :: forall st wal k s e a . FSMStore st k s e a => FSMHandle st wal k s e a -> k -> s -> IO Bool post FSMHandle{..} k s0 = fsmCreate fsmStore (mkInstance k s0 [] :: Instance k s e a) >>= \case Nothing -> return True Just s -> hPutStrLn stderr s >> return False -- |Concurrent updates will be serialised by Postgres. -- Returns True when the state transition has been successfully computed -- and actions have been scheduled, now or at any time in the past. -- Returns False on failure. patch :: forall st wal k s e a . (FSMStore st k s e a, MealyInstance k s e a, FSMKey k) => FSMHandle st wal k s e a -> k -> [Msg e] -> IO Bool patch h@FSMHandle{..} k es = do openTxn walStore k status <- handle (\(e::SomeException) -> hPutStrLn stderr (show e) >> return MealyError) (fsmUpdate fsmStore k ((patchPhase1 fsmTable es) :: MachineTransformer s e a)) if status /= MealyError then recover h k >> return True else return False -- |Recovering is the process of asynchronously applying Actions. It is performed -- immediately after the synchronous part of an update and, on failure, retried until it -- succeeds or the retry limit is hit. recover :: forall st wal k s e a . (FSMStore st k s e a, MealyInstance k s e a, FSMKey k) => FSMHandle st wal k s e a -> k -> IO () recover h@FSMHandle{..} k | retryCount == 0 = hPutStrLn stderr $ "Alarma! Recovery retries for " ++ Text.unpack (toText k) ++ " exhausted. Giving up!" | otherwise = void $ forkFinally (timeout (effTimeout*10^6) (fsmUpdate fsmStore k (patchPhase2 fsmTable :: MachineTransformer s e a))) -- (patchPhase2 fsmTable)) (\case Left exn -> do -- the damn thing crashed, print log and try again hPutStrLn stderr $ "Exception occurred while trying to recover " ++ Text.unpack (toText k) hPrint stderr exn recover h{retryCount = retryCount - 1} k Right Nothing -> do -- We hit the timeout. Try again until we hit the retry limit. hPutStrLn stderr $ "Timeout while trying to recover " ++ Text.unpack (toText k) recover h{retryCount = retryCount - 1} k Right (Just Done) -> closeTxn walStore k -- All good. Right (Just Pending) -> -- Some actions did not complete successfully. recover h{retryCount = retryCount - 1} k) -- |During certain long-lasting failures, like network outage, the retry limit of Actions will be exhausted. -- You should regularly, e.g. ever 10 minutes, call this function to clean up those hard cases. recoverAll :: forall st wal k s e a . (MealyInstance k s e a) => FSMHandle st wal k s e a -> IO () recoverAll h@FSMHandle{..} = do wals <- walScan walStore effTimeout mapM_ (recover h . walId) wals -- |A helper that is sometimes useful upsert :: forall st wal k s e a . MealyInstance k s e a => FSMStore st k s e a => FSMHandle st wal k s e a -> k -> s -> [Msg e] -> IO () upsert h k s es = do ms <- get h k maybe (post h k s >> void (patch h k es)) (\_s -> void $ patch h k es) ms