packages feed

eventuo11y-0.1.0.0: src/Observe/Event/Implementation.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Description : Interface for implementing EventBackends
-- Copyright   : Copyright 2022 Shea Levy.
-- License     : Apache-2.0
-- Maintainer  : shea@shealevy.com
--
-- This is the primary module needed to write new 'EventBackend's.
module Observe.Event.Implementation
  ( EventBackend (..),
    EventImpl (..),

    -- * OnceFlags

    -- | Generic helper to make operations idempotent.
    OnceFlag (..),
    FlagState (..),
    runOnce,
    hoistOnceFlag,
    alwaysNewOnceFlag,
    newOnceFlagIO,
  )
where

import Control.Concurrent.MVar
import Control.Exception
import Data.Functor

-- | A backend for creating t'Observe.Event.Event's.
--
-- Different 'EventBackend's will be used to emit instrumentation to
-- different systems. Multiple backends can be combined with
-- 'Observe.Event.pairEventBackend'.
--
-- A simple 'EventBackend' for logging to a t'System.IO.Handle' can be
-- created with 'Observe.Event.Render.IO.JSON.jsonHandleBackend'.
--
-- Typically the entrypoint for some eventuo11y-instrumented code will
-- take an 'EventBackend', polymorphic in @r@ and possibly @m@. Calling
-- code can use 'Observe.Event.subEventBackend' to place the resulting
-- events in its hierarchy.
--
-- From an 'EventBackend', new events can be created via selectors
-- (of type @s f@ for some field type @f@), typically with the
-- [resource-safe allocation functions](Observe-Event.html#g:resourcesafe).
-- Selectors are values which designate the general category of event
-- being created, as well as the type of fields that can be added to it.
-- For example, a web service's selector type may have a @ServicingRequest@
-- constructor, whose field type includes a @ResponseCode@ constructor which
-- records the HTTP status code.
--
-- Selectors are intended to be of a domain specific type per unit of
-- functionality within an instrumented codebase, implemented as a GADT
-- (but see t'Observe.Event.Dynamic.DynamicEventSelector' for a generic option).
--
-- Implementations must ensure that 'EventBackend's and their underlying t'Observe.Event.Event's
-- are safe to use across threads.
--
-- [@m@]: The monad we're instrumenting in.
-- [@r@]: The type of event references used in this 'EventBackend'. See 'Observe.Event.reference'.
-- [@s@]: The type of event selectors.
data EventBackend m r s = EventBackend
  { -- | Create a new 'EventImpl' corresponding to the given selector.
    newEventImpl :: !(forall f. s f -> m (EventImpl m r f)),
    -- | Allocate a new 'OnceFlag' in our monad.
    newOnceFlag :: !(m (OnceFlag m))
  }

-- | The internal implementation of an t'Observe.Event.Event'.
--
-- All fields have corresponding [event manipulation functions](Observe-Event.html#g:eventmanip),
-- except that 'finalizeImpl' and 'failImpl' can assume that they will only ever be called
-- once (i.e., 'EventImpl' implementations do __not__ have to implement locking internally).
data EventImpl m r f = EventImpl
  { referenceImpl :: !r,
    addFieldImpl :: !(f -> m ()),
    addParentImpl :: !(r -> m ()),
    addProximateImpl :: !(r -> m ()),
    finalizeImpl :: !(m ()),
    failImpl :: !(Maybe SomeException -> m ())
  }

-- | The state of a 'OnceFlag'
data FlagState
  = -- | The flag was not set, but is now
    NewlySet
  | -- | The flag was already set
    AlreadySet

-- | A flag to ensure only one operation from some class is performed, once.
--
-- Typically consumed via 'runOnce'
newtype OnceFlag m = OnceFlag
  { -- | Get the state of the 'OnceFlag', and set the flag.
    --
    -- This operation should be atomic, and ideally would only
    -- return 'NewlySet' once. In monads that don't support it,
    -- at a minimum it must be monotonic (once one caller gets
    -- 'AlreadySet', all callers will).
    checkAndSet :: m FlagState
  }

-- | Run an operation if no other operations using this
-- 'OnceFlag' have run.
runOnce :: (Monad m) => OnceFlag m -> m () -> m ()
runOnce f go =
  checkAndSet f >>= \case
    NewlySet -> go
    AlreadySet -> pure ()

-- | A 'OnceFlag' in 'IO' using an 'MVar'.
newOnceFlagIO :: IO (OnceFlag IO)
newOnceFlagIO = do
  flag <- newEmptyMVar
  pure $
    OnceFlag $
      tryPutMVar flag () <&> \case
        False -> AlreadySet
        True -> NewlySet

-- | A 'OnceFlag' which is always 'NewlySet'.
--
-- Only safe to use if the operations to be guarded
-- by the flag are already idempotent.
alwaysNewOnceFlag :: (Applicative m) => OnceFlag m
alwaysNewOnceFlag = OnceFlag $ pure NewlySet

-- | Hoist a 'OnceFlag' along a given natural transformation into a new monad.
hoistOnceFlag ::
  -- | Natural transformation from @f@ to @g@
  (forall x. f x -> g x) ->
  OnceFlag f ->
  OnceFlag g
hoistOnceFlag nt (OnceFlag cs) = OnceFlag (nt cs)