packages feed

neptune-backend-0.1.1: lib/Neptune/Client.hs

{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
module Neptune.Client where

import           Control.Concurrent        (ThreadId, forkIO, killThread)
import           Control.Lens
import qualified Data.Text.Lazy            as TL
import qualified Data.Text.Lazy.Encoding   as TL
import           Data.Time.Clock           (getCurrentTime)
import qualified Data.UUID                 as UUID (toText)
import           Data.UUID.V4              as UUID
import qualified Network.HTTP.Client       as NH
import qualified Network.HTTP.Client.TLS   as NH
import           RIO                       hiding (Lens', (^.))
import qualified RIO.HashMap               as M
import qualified RIO.Text                  as T
import           System.Envy               (decodeEnv)

import qualified Neptune.Backend.API       as NBAPI
import           Neptune.Backend.Client
import           Neptune.Backend.Core
import           Neptune.Backend.MimeTypes
import           Neptune.Backend.Model     hiding (Experiment, Parameter)
import           Neptune.Backend.ModelLens
import           Neptune.Channel
import           Neptune.OAuth
import           Neptune.Session
import           Neptune.Utils


data Parameter = ExperimentParamS Text Text
    | ExperimentParamD Text Double

data Experiment = Experiment
    { _exp_experiment_id :: ExperimentId
    , _exp_outbound_q    :: TChan DataPointAny
    , _exp_user_channels :: ChannelHashMap
    , _exp_transmitter   :: ThreadId
    }

makeLenses ''Experiment


createExperiment :: HasCallStack
                 => NeptuneSession
                 -> Maybe Text
                 -> Maybe Text
                 -> [Parameter]
                 -> [(Text, Text)]
                 -> [Text]
                 -> IO Experiment
createExperiment session@NeptuneSession{..} name description params props tags = do
    -- TODO support git_info
    -- TODO support uploading source code
    -- TODO support abort callback. W/o a callback, the app will
    --      continue running when you click abort in the web console
    params <- mapM _mkParameter params
    exp    <- _neptune_dispatch $ NBAPI.createExperiment
                (ContentType MimeJSON)
                (Accept MimeJSON)
                (mkExperimentCreationParams
                    (_neptune_project ^. projectWithRoleDTOIdL)
                    (map (uncurry KeyValueProperty) props)
                    "" -- legacy
                    params
                    "command" -- legacy
                    (fromMaybe "Untitled" name)
                    tags){ experimentCreationParamsDescription = description }
    let exp_id = ExperimentId (exp ^. experimentIdL)
    chan <- newTChanIO
    user_channels <- newTVarIO M.empty
    transmitter_thread <- forkIO $ transmitter session exp_id chan user_channels
    return $ Experiment exp_id chan user_channels transmitter_thread

    where
        _mkParameter (ExperimentParamS name value) = do
            _id <- UUID.toText <$> UUID.nextRandom
            return $ mkParameter name ParameterTypeEnum'String _id value
        _mkParameter (ExperimentParamD name value) = do
            _id <- UUID.toText <$> UUID.nextRandom
            return $ mkParameter name ParameterTypeEnum'Double _id (tshow value)

nlog :: (HasCallStack, NeptDataType a) => Experiment -> Text -> a -> IO ()
nlog exp name value = do
    now <- getCurrentTime
    let chan = exp ^. exp_outbound_q
        dat  = DataPointAny $ DataPoint name now value
    atomically $ writeTChan chan dat

withNept :: Text -> (NeptuneSession -> Experiment -> IO a) -> IO a
withNept project_qualified_name act = do
    ses <- initNept project_qualified_name
    exp <- createExperiment ses Nothing Nothing [] [] []

    result <- try (act ses exp)
    case result of
      Left (e :: SomeException) -> do
          teardownNept ses exp ExperimentState'Failed (T.pack $ displayException e)
          throwM e
      Right a -> do
          teardownNept ses exp ExperimentState'Succeeded ""
          return a

initNept :: HasCallStack => Text -> IO NeptuneSession
initNept project_qualified_name = do
    ct@ClientToken{..} <- decodeEnv >>= either throwString return

    mgr <- NH.newManager NH.tlsManagerSettings
    config0 <- withStderrLogging =<< newConfig

    let api_endpoint = TL.encodeUtf8 (TL.fromStrict _ct_api_url)
        config = config0 { configHost = api_endpoint }

    let dispatch = dispatchMime mgr config{configValidateAuthMethods = False}
                    >=> handleMimeError

    oauth_token <- dispatch  $ NBAPI.exchangeApiToken (Accept MimeJSON) (XNeptuneApiToken _ct_token)
    (refresh_thread, oauth_session) <- oauth2_setup (oauth_token ^. neptuneOauthTokenAccessTokenL)
                                                    (oauth_token ^. neptuneOauthTokenRefreshTokenL)

    -- TODO there is a chance that the access token gets invalid right after readMVar
    let dispatch :: (HasCallStack, Produces req accept, MimeUnrender accept res, MimeType contentType)
                 => NeptuneBackendRequest req contentType res accept -> IO res
        dispatch req = do
            access_token <- readMVar oauth_session <&> _oas_access_token
            resp <- dispatchMime mgr (config `addAuthMethod` AuthOAuthOauth2 access_token) req
            handleMimeError resp

    proj <- dispatch $ NBAPI.getProject (Accept MimeJSON) (ProjectIdentifier project_qualified_name)
    return $ NeptuneSession
        { _neptune_http_manager = mgr
        , _neptune_client_token = ct
        , _neptune_config = config
        , _neptune_oauth2 = oauth_session
        , _neptune_oauth2_refresh = refresh_thread
        , _neptune_project = proj
        , _neptune_dispatch = dispatch
        }

teardownNept :: NeptuneSession -> Experiment -> ExperimentState -> Text -> IO ()
teardownNept NeptuneSession{..} experiment state msg = do
    killThread $ experiment ^. exp_transmitter
    killThread $ _neptune_oauth2_refresh

    _neptune_dispatch $ NBAPI.markExperimentCompleted
        (ContentType MimeJSON)
        (Accept MimeNoContent)
        (mkCompletedExperimentParams state msg)
        (experiment ^. exp_experiment_id) :: IO NoContent

    return ()