packages feed

groot-0.0.1.0: src/Groot/Core/Events.hs

{-# LANGUAGE ExplicitForAll        #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}

module Groot.Core.Events
       ( serviceEventLog
       , clusterServiceEventLog
       , printEventSink
       ) where

import           Control.Applicative
import           Control.Concurrent             (killThread)
import           Control.Concurrent.STM
import           Control.Concurrent.STM.Delay
import           Control.Concurrent.STM.TBMChan
import           Control.Exception.Lens
import qualified Control.Exception.Lifted       as Lifted
import           Control.Lens                   hiding (argument)
import           Control.Monad
import           Control.Monad.Catch
import           Control.Monad.Morph
import           Control.Monad.Reader
import           Control.Monad.Trans.Control
import           Control.Monad.Trans.Resource
import           Control.Monad.Trans.State.Lazy
import           Data.Conduit
import qualified Data.Conduit.List              as CL
import           Data.Maybe                     (listToMaybe)
import           Data.Text                      (Text)
import qualified Data.Text                      as T
import           Data.Time
import           Data.Typeable
import           Network.AWS                    hiding (await)
import qualified Network.AWS.ECS                as ECS

import           Groot.AWS
import           Groot.Console
import           Groot.Data.Conduit.STM
import           Groot.Data.Filter
import           Groot.Data.Text
import           Groot.Exception
import           Groot.Types

formatEventTime :: MonadIO m => UTCTime -> m Text
formatEventTime time = do
  dt <- liftIO $ utcToLocalZonedTime time
  return . T.pack $ formatTime defaultTimeLocale "%d/%m/%Y %T" dt

pollServiceEvents :: (MonadReader e m, MonadBaseControl IO m, MonadIO m, HasEnv e)
                  => ContainerServiceCoords
                  -> Bool
                  -> Int
                  -> TBMChan ECS.ServiceEvent
                  -> (TBMChan ECS.ServiceEvent -> STM ())
                  -> m ()
pollServiceEvents (ContainerServiceCoords serviceRef clusterRef) inf lastN chan onComplete =
  evalStateT loop Nothing
  where loop :: (MonadReader e m, MonadIO m, HasEnv e) => StateT (Maybe UTCTime) m ()
        loop = do
          env           <- lift $ ask
          lastEventTime <- get
          events        <- liftIO . runResourceT . runAWS env $ serviceEvents lastEventTime
          liftIO $ forM_ (reverse events) $ atomically . tryWriteTBMChan chan
          if inf then do
            delay <- liftIO $ newDelay 2000000
            let nextTime = listToMaybe events >>= view ECS.seCreatedAt
            liftIO . atomically $ waitDelay delay
            put $ nextTime <|> lastEventTime
            loop
          else liftIO . atomically $ onComplete chan

        serviceEvents :: MonadAWS m => Maybe UTCTime -> m [ECS.ServiceEvent]
        serviceEvents lastEventTime = do
          service  <- getService serviceRef (Just clusterRef)
          service' <- if (matches isActiveContainerService service)
                      then return service
                      else throwM $ inactiveService serviceRef clusterRef
          events  <- return $ service' ^. ECS.csEvents
          return $ case lastEventTime of
            Nothing -> take lastN events
            Just t  -> takeWhile (\ev -> maybe False (> t) $ ev ^. ECS.seCreatedAt) events

serviceEventLog :: Typeable mi
                => MonadResource mi
                => MonadBaseControl IO mi
                => MonadCatch mi
                => MonadReader e mi
                => MonadConsole mi
                => MonadIO mo
                => HasEnv e
                => [ContainerServiceCoords]
                -> Bool
                -> Int
                -> mi (Source mo ECS.ServiceEvent)
serviceEventLog coords inf lastN = do
  chan     <- liftIO . atomically $ newTBMChan 500
  refCount <- liftIO . atomically . newTVar $ length coords
  regs     <- forM coords (forkEventStream chan refCount)
  return $ chanSource chan readTBMChan (\ch -> do liftIO . atomically $ closeTBMChan ch
                                                  mapM_ release regs)

  where
    forkEventStream chan refCount serviceCoord = Lifted.mask_ $ do
      threadId <- runResourceT $ resourceForkIO $ lift $ publishInto chan serviceCoord $ decRefCount refCount
      register . killThread $ threadId

    modifyTVar'' :: TVar a -> (a -> a) -> STM a
    modifyTVar'' tvar f = do
      x <- f <$> readTVar tvar
      writeTVar tvar x
      return x

    decRefCount :: TVar Int -> TBMChan a -> STM ()
    decRefCount tvar chan = do
      n <- modifyTVar'' tvar (subtract 1)
      when (n == 0) $ closeTBMChan chan

    handleStreamErrors :: (Typeable m, MonadCatch m, MonadConsole m) => m () -> m ()
    handleStreamErrors action = catches action [
        handler _InactiveService (\_ -> pure ())
      , handler _TransportError  handleHttpException
      , handler _ServiceError    handleServiceError
      ]

    publishInto :: (Typeable m, MonadReader e m, MonadCatch m, MonadBaseControl IO m, MonadIO m, MonadConsole m, HasEnv e)
                => TBMChan ECS.ServiceEvent
                -> ContainerServiceCoords
                -> (TBMChan ECS.ServiceEvent -> STM ())
                -> m ()
    publishInto chan crds onComplete =
      handleStreamErrors $ pollServiceEvents crds inf lastN chan onComplete

clusterServiceEventLog :: Typeable mi
                       => MonadResource mi
                       => MonadBaseControl IO mi
                       => MonadCatch mi
                       => MonadIO mo
                       => MonadConsole mi
                       => MonadReader e mi
                       => HasEnv e
                       => [ClusterRef]
                       -> Bool
                       -> Int
                       -> mi (Source mo ECS.ServiceEvent)
clusterServiceEventLog clusterRefs inf lastN = do
  env    <- ask
  coords <- runResourceT . runAWS env $ allServiceCoords
  serviceEventLog coords inf lastN
  where clusterServiceCoords :: MonadAWS m => ClusterRef -> m [ContainerServiceCoords]
        clusterServiceCoords cref = sourceToList $ fetchServices cref
          =$= filterC isActiveContainerService
          =$= CL.mapMaybe serviceCoords

        allServiceCoords :: MonadAWS m => m [ContainerServiceCoords]
        allServiceCoords = do
          coords <- concat <$> mapM clusterServiceCoords clusterRefs
          return coords

printEvent :: MonadIO m => ECS.ServiceEvent -> m ()
printEvent event = do
  eventTime <- maybe (return "") formatEventTime $ event ^. ECS.seCreatedAt
  displayLn $ (styled yellowStyle eventTime) <+> (styleless $ maybe "" id $ event ^. ECS.seMessage)

printEventSink :: MonadIO m => Sink ECS.ServiceEvent m ()
printEventSink = do
  mevent <- await
  case mevent of
    Just event -> do
      printEvent event
      printEventSink
    Nothing -> return ()