salmon-ops-0.2.0.0: src/Salmon/Builtin/Guarded.hs
{- | Guarding an operation that cannot be undone: a major upgrade, a restart
of the member everything else follows.
The pattern is written here once, as a combinator over a node somebody
already wrote, and is three rules:
1. __It starts only when its preconditions are met.__ They are folded into
the node's @check@: a node whose effect is not in place and whose
preconditions are unmet answers 'Salmon.Actions.UpDown.Unknown' ("ran and
could not tell", the verdict for a transitional state), and
"Salmon.Actions.Upkeep" keeps asking and starts nothing. They are asked
again at the top of @up@, which throws 'PreconditionUnmet' rather than
act, because the one-shot drivers map 'Salmon.Actions.UpDown.Unknown' to
"run @up@": under @run up@ the operation is refused, reported failed, and
its dependants are blocked.
2. __A failure parks it.__ The node's
'Salmon.Op.Supervision.supGiveUpAfter' is set (to one failure unless
'guardingAttempts' says otherwise), so "Salmon.Actions.Upkeep" stops
instead of retrying, and only an operator's
'Salmon.Op.Mailbox.Force' or 'Salmon.Op.Mailbox.Recheck' starts it over.
A refusal to start (rule 1) is not a failure and is not counted.
3. __Its siblings hold still while it runs.__ 'guardingHolds' names them;
"Salmon.Actions.Upkeep" pauses each one, waits until they have all
stopped, runs @up@, and resumes them when the operation completes or is
parked.
What this does not do, so that nobody assumes it:
* It does not guard 'Salmon.Builtin.Extension.managed'. A node that /is/ a
running process has no @up@ for a precondition to stand in front of.
* 'Salmon.Op.Mailbox.Force' does not bypass the preconditions: it overrides
the check, and @up@ asks them again.
* Rules 2 and 3 are "Salmon.Actions.Upkeep"'s. A one-shot pass makes one
attempt by construction and pauses nobody; there, only an edge orders two
nodes.
* The park lives in the node's machine, not on disk. See
@resources\/module-notes.md@ on what that means under @run serve@.
-}
module Salmon.Builtin.Guarded (
guarded,
Guarding (..),
defaultGuarding,
-- * Re-exported from "Salmon.Op.Guard"
Precondition (..),
PreconditionUnmet (..),
Guard (..),
guardOf,
isGuarded,
) where
import Control.Exception (SomeAsyncException, SomeException, fromException, throwIO, try)
import Data.Dynamic (Dynamic, fromDynamic, toDyn)
import Data.Maybe (isNothing)
import qualified Data.Text as Text
import Salmon.Actions.UpDown (CheckResult (..))
import Salmon.Builtin.Extension (Extension)
import qualified Salmon.Builtin.Extension as Extension
import Salmon.Op.Guard
import Salmon.Op.Ref (Ref)
import Salmon.Op.Supervision (Supervision (..), supervised, supervisionOf)
-- | How an operation is guarded, beyond its preconditions.
data Guarding = Guarding
{ guardingHolds :: [Ref]
-- ^ the nodes to hold still while the operation runs. See 'Guard'.
, guardingAttempts :: Int
-- ^ how many consecutive failures park the node. One, by default: an
-- operation that cannot be undone is not one to retry on a timer.
}
deriving (Show, Eq)
-- | Holds nobody, parks at the first failure.
defaultGuarding :: Guarding
defaultGuarding = Guarding [] 1
{- | Guard a node. Apply it to the one node that is the risky operation, not
to an 'Salmon.Builtin.Extension.Op' through @fmap@ (which reaches its
dependencies too).
The precondition action is run at @check@ and @up@ time, never at graph
build, and what it returns is public. If it throws, the preconditions count
as unmet: a probe that could not be asked is not a reason to start.
The node keeps whatever 'Supervision' it declared, with only
'supGiveUpAfter' replaced.
-}
guarded :: Guarding -> IO Precondition -> Extension -> Extension
guarded g preconditions e =
e
{ Extension.check = guardedCheck
, Extension.up = guardedUp
, Extension.dynamics =
supervised policy{supGiveUpAfter = Just (max 1 g.guardingAttempts)}
: toDyn (Guard g.guardingHolds)
: filter notSupervision (Extension.dynamics e)
}
where
(policy, _) = supervisionOf e
notSupervision :: Dynamic -> Bool
notSupervision d = isNothing (fromDynamic d :: Maybe Supervision)
ask :: IO Precondition
ask = do
answer <- try @SomeException preconditions
case answer of
Right p -> pure p
Left err
| Just (_ :: SomeAsyncException) <- fromException err -> throwIO err
| otherwise -> pure (Unmet ("could not be asked: " <> Text.pack (show err)))
-- the effect being in place outranks the preconditions: an upgrade that
-- has happened is done whatever the cluster looks like now.
guardedCheck :: IO CheckResult
guardedCheck = do
verdict <- Extension.check e
case verdict of
Success -> pure verdict
Skipped -> pure verdict
Completed -> pure verdict
_ -> do
p <- ask
pure $ case p of
Met -> verdict
Unmet _ -> Unknown
guardedUp :: IO ()
guardedUp = do
p <- ask
case p of
Met -> Extension.up e
Unmet why -> throwIO (PreconditionUnmet why)