packages feed

mole-0.0.7: src/Data/Mole/Core.hs

module Data.Mole.Core where


import           Control.Concurrent
import           Control.Concurrent.STM
import           Control.Monad
import           Control.Monad.Trans.Maybe

import           Data.Map (Map)
import qualified Data.Map as M

import           Data.Set (Set)
import qualified Data.Set as S

import           Data.ByteString (ByteString)
import qualified Data.Text as T

import           Data.Maybe
import           Data.Time

import           Data.Mole.Types
import           Data.Mole.Builder.External

import           System.Environment
import           System.IO (hFlush, stdout)

import qualified Network.Kraken as K



padL :: Int -> String -> String
padL n s
    | length s < n  = s ++ replicate (n - length s) ' '
    | otherwise     = s

newHandle :: Config -> IO Handle
newHandle config = do
    st <- newTVarIO $ State Nothing (return ()) M.empty
    l <- newTMVarIO ()

    msgs <- newTQueueIO
    void $ forkIO $ forever $ do
        (Message time aId msg) <- atomically $ readTQueue msgs
        putStrLn $ mconcat
            [ formatTime defaultTimeLocale "%H:%M:%S" time
            , " [ " <> take 24 (padL 24 (T.unpack $ unAssetId aId)) <> " ] "
            , msg
            ]
        hFlush stdout

    e <- newTQueueIO
    void $ forkIO $ forever $ do
        join $ atomically $ readTQueue e

    kH <- runMaybeT $ do
        apiKey <- MaybeT $ lookupEnv "KRAKEN_API_KEY"
        apiSecret <- MaybeT $ lookupEnv "KRAKEN_API_SECRET"

        MaybeT $ Just <$> K.newHandle (K.Config apiKey apiSecret)

    let h = Handle st msgs e kH l


    -- This background thread periodically checks if there are any assets
    -- marked as dirty and forks a build thread for each.
    tId <- forkIO $ forever $ do

        -- Get a list of dirty assets. Those are the ones which we need to
        -- rebuild. The check runs in a STM transaction, and will block until
        -- at least one asset is dirty. Much efficient, wow.
        dirtyAssetIds <- atomically $ do
            s <- readTVar st
            let assetIds = M.keys $ M.filter ((==) Dirty . arsState) (assets s)
            if length assetIds == 0 then retry else return assetIds

        forM_ dirtyAssetIds $ \aId -> do
            -- First we have to mark the asset as being built. This is to avoid
            -- forking two or more build threads for the same asset.
            markBuilding h aId

            forkIO $ do
                assetDef <- lookupAssetDefinition config h aId
                case assetDef of
                    Nothing -> do
                        -- failBuild h aId (AssetNotFound aId)
                        logMessage h aId $ "Asset not found, treating as external: " ++ show aId
                        buildAsset h aId $ AssetDefinition (externalBuilder $ PublicIdentifier $ unAssetId aId) id (\_ _ _ -> return ())
                    Just ad -> do
                        -- logMessage h aId $ "Building"
                        buildAsset h aId ad

    atomically $ modifyTVar st (\s -> s { dispatcherThreadId = Just tId })

    return h

logMessage :: Handle -> AssetId -> String -> IO ()
logMessage h aId msg = do
    now <- getCurrentTime
    atomically $ writeTQueue (messages h) (Message now aId msg)


adjustAssetRuntimeState :: Handle -> AssetId -> (AssetRuntimeState -> AssetRuntimeState) -> IO ()
adjustAssetRuntimeState h aId f = atomically $ do
    modifyTVar (state h) $ \s -> s { assets = M.adjust f aId (assets s) }

insertAssetRuntimeStateWith :: Handle -> AssetId -> (AssetRuntimeState -> AssetRuntimeState -> AssetRuntimeState) -> AssetRuntimeState -> IO ()
insertAssetRuntimeStateWith h aId f d = atomically $ do
    modifyTVar (state h) $ \s -> s { assets = M.insertWith f aId d (assets s) }


updateMetadata :: Handle -> AssetId -> Set FilePath -> Set AssetId -> ByteString -> Map AssetId PublicIdentifier -> IO ()
updateMetadata h aId src ds fp rd = adjustAssetRuntimeState h aId $ \ars -> ars
    { arsSources = src
    , arsDependencySet = ds
    , arsSource = Just (fp, rd)
    }


buildIfNecessary :: Handle -> AssetId -> IO ()
buildIfNecessary h aId = insertAssetRuntimeStateWith h aId adj (assetRuntimeState Dirty)
  where
    adj _ ars = case arsState ars of
        Building _  -> ars
        Completed _ -> ars
        _           -> ars { arsState = Dirty }


markDirty :: Handle -> AssetId -> IO ()
markDirty h aId = insertAssetRuntimeStateWith h aId f (assetRuntimeState Dirty)
  where f _ ars = ars { arsState = Dirty }


markBuilding :: Handle -> AssetId -> IO ()
markBuilding h aId = do
    s <- Building <$> getCurrentTime
    insertAssetRuntimeStateWith h aId (\_ ars -> ars { arsState = s }) (assetRuntimeState s)


failBuild :: Handle -> AssetId -> Error -> IO ()
failBuild h aId err = do
    logMessage h aId $ "Failure: " ++ show err
    adjustAssetRuntimeState h aId $ \ars -> ars { arsState = Failed err }
    rebuildReverseDependencies h aId

finishBuilding :: Handle -> AssetId -> Result -> IO ()
finishBuilding h aId res = do
    now <- getCurrentTime

    let diff (Building t0) = diffUTCTime now t0
        diff _             = fromIntegral (0 :: Int)

    adjustAssetRuntimeState h aId $ \ars -> ars
        { arsState = Completed (diff $ arsState ars)
        , arsResult = Just res
        }

    mbArs <- atomically $ do
        s <- readTVar (state h)
        return $ M.lookup aId (assets s)

    case mbArs of
        Nothing -> return ()
        Just ars -> case arsState ars of
            Completed td -> logMessage h aId $ "Build time: " ++ show td
            _ -> return ()


    -- Go through all reverse dependencies and mark them as dirty.
    rebuildReverseDependencies h aId


isBuilding :: AssetState -> Bool
isBuilding (Building _) = True
isBuilding _            = False

isFailed :: AssetState -> Bool
isFailed (Failed _) = True
isFailed _          = False

rebuildReverseDependencies :: Handle -> AssetId -> IO ()
rebuildReverseDependencies h aId = do
    s <- atomically $ readTVar (state h)
    forM_ (M.toList $ assets s) $ \(aId', ars) -> do
        when ((not $ isBuilding $ arsState ars) && S.member aId (arsDependencySet ars)) $ do
            markDirty h aId'


-- | Wait until the set of assets is built, and return the corresponding
-- results. If any of the assets fails to build (for whatever reason), then
-- immediately abort and return the reason.
require :: Handle -> Set AssetId -> IO (Either Error (Map AssetId Result))
require h assetIds = do
    -- Mark assets as dirty if they are not comleted yet.
    forM_ assetIds $ \dep -> do
        buildIfNecessary h dep

    -- Wait for the dependencies to have completed building.
    atomically $ do
        s <- readTVar (state h)

        -- All dependencies which are relevant.
        let allDependencies = M.filterWithKey (\aId _ -> S.member aId assetIds) (assets s)

        -- The dependencies which are completed and for which we have a result.
        let completedDependencies = flip M.mapMaybe allDependencies $ \ars -> case (arsState ars, arsResult ars) of
                (Completed _, Just res) -> Just res
                _                       -> Nothing

        -- A more accurate check would be 'assetIds == M.keysSet completedDependencies'.
        -- Though comparing the length is probably faster.
        if length completedDependencies == length assetIds
            then return $ Right $ completedDependencies
            else if any (isFailed . arsState) (M.elems allDependencies)
                then return $ Left DependencyFailed
                else retry


assetsByPublicIdentifier :: State -> PublicIdentifier -> [(AssetId, Result)]
assetsByPublicIdentifier st pubId = filter (\(_,res) -> publicIdentifier res == pubId) $
    catMaybes $ map f $ M.assocs $ assets st
  where f (aId, AssetRuntimeState (Completed _) _ _ _ (Just res)) = Just (aId, res)
        f _ = Nothing

assetByPublicIdentifier :: State -> PublicIdentifier -> Maybe Result
assetByPublicIdentifier st pubId = lookup pubId $ catMaybes $ map f $ M.elems $ assets st
  where f (AssetRuntimeState (Completed _) _ _ _ (Just res)) = Just (publicIdentifier res, res)
        f _ = Nothing



lookupAssetDefinition :: Config -> Handle -> AssetId -> IO (Maybe AssetDefinition)
lookupAssetDefinition config h aId = case M.lookup aId (assetDefinitions config) of
    Just ad -> return $ Just ad
    Nothing -> autoDiscovery config h aId


buildAsset :: Handle -> AssetId -> AssetDefinition -> IO ()
buildAsset h aId ad = do
    Builder src depSet cont fp <- createBuilder ad h aId

    eitherResolvedDeps <- require h depSet
    case eitherResolvedDeps of
        Left e -> failBuild h aId e

        Right resolvedDeps -> do
            let sourceDeps = M.map publicIdentifier resolvedDeps

            -- First check if we actually need to rebuild the asset. If the source
            -- fingerprint is still the same then we can skip directly to 'Completed'.
            needsRebuild <- atomically $ do
                s <- readTVar (state h)
                return $ case M.lookup aId (assets s) of
                    Just (AssetRuntimeState _ _ _ (Just (sfp, srd)) (Just _)) -> sfp /= fp || sourceDeps /= srd
                    _                                                         -> True

            -- Eagerly update the metadata, even if we don't have to rebuild the asset.
            -- When deciding whether to rebuild the asset or not, the only thing that
            -- matters is the fingerprint. But the builder may have an updated or more
            -- accurate set of dependencies now, and we do want to update that.
            updateMetadata h aId src depSet fp sourceDeps


            if not needsRebuild
                then do
                    logMessage h aId $ "Skip"

                    now <- getCurrentTime

                    let diff (Building t0) = diffUTCTime now t0
                        diff _             = fromIntegral (0 :: Int)

                    adjustAssetRuntimeState h aId $ \ars ->
                        ars { arsState = Completed (diff $ arsState ars) }

                else do
                    -- logger lock $ "Got all dependencies of " ++ show aId
                    -- logger lock $ resolvedDeps
                    case cont (M.map publicIdentifier resolvedDeps) of
                        Left e -> failBuild h aId e
                        Right result1@(Result pub _) -> do
                            let result = result1 { publicIdentifier = transformPublicIdentifier ad pub }
                            -- logger lock $ "Pub: " ++ (publicIdentifier result)
                            -- logger lock $ res

                            atomically $ writeTQueue (emitStream h) $ emitResult ad h aId result
                            finishBuilding h aId result