haxl 0.5.1.0 → 2.0.0.0
raw patch · 44 files changed
+3547/−2190 lines, 44 filesdep +semigroupsdep +stmdep ~HUnitdep ~aesondep ~basenew-component:exe:cachebenchPVP ok
version bump matches the API change (PVP)
Dependencies added: semigroups, stm
Dependency ranges changed: HUnit, aeson, base, exceptions, hashable, time
API changes (from Hackage documentation)
- Haxl.Core: DataSourceRoundStats :: Int -> Maybe Microseconds -> Maybe Int -> Maybe Int -> DataSourceRoundStats
- Haxl.Core: RoundStats :: Microseconds -> Int -> HashMap Text DataSourceRoundStats -> RoundStats
- Haxl.Core: [dataSourceAllocation] :: DataSourceRoundStats -> Maybe Int
- Haxl.Core: [dataSourceFailures] :: DataSourceRoundStats -> Maybe Int
- Haxl.Core: [dataSourceFetches] :: DataSourceRoundStats -> Int
- Haxl.Core: [dataSourceTime] :: DataSourceRoundStats -> Maybe Microseconds
- Haxl.Core: [roundAllocation] :: RoundStats -> Int
- Haxl.Core: [roundDataSources] :: RoundStats -> HashMap Text DataSourceRoundStats
- Haxl.Core: [roundTime] :: RoundStats -> Microseconds
- Haxl.Core: data DataSourceRoundStats
- Haxl.Core: data RoundStats
- Haxl.Core: newEmptyResult :: IO (ResultVar a)
- Haxl.Core: newResult :: a -> IO (ResultVar a)
- Haxl.Core: ppDataSourceRoundStats :: DataSourceRoundStats -> String
- Haxl.Core: ppRoundStats :: RoundStats -> String
- Haxl.Core: profileCache :: Profile -> DataCache (Constant Round)
- Haxl.Core: profileRound :: Profile -> Round
- Haxl.Core: takeResult :: ResultVar a -> IO (Either SomeException a)
- Haxl.Core: tryReadResult :: ResultVar a -> IO (Maybe (Either SomeException a))
- Haxl.Core: tryTakeResult :: ResultVar a -> IO (Maybe (Either SomeException a))
- Haxl.Core.Monad: cacheRequest :: Request req a => req a -> Either SomeException a -> GenHaxl u ()
- Haxl.Core.Monad: cacheResult :: Request r a => r a -> IO a -> GenHaxl u a
- Haxl.Core.Monad: cacheResultWithShow :: (Eq (r a), Hashable (r a), Typeable (r a)) => ShowReq r a -> r a -> IO a -> GenHaxl u a
- Haxl.Core.Monad: cachedComputation :: forall req u a. (Eq (req a), Hashable (req a), Typeable (req a)) => req a -> GenHaxl u a -> GenHaxl u a
- Haxl.Core.Monad: dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a
- Haxl.Core.Monad: dataFetchWithShow :: (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a)) => ShowReq r a -> r a -> GenHaxl u a
- Haxl.Core.Monad: infixr 4 `pOr`
- Haxl.Core.Monad: infixr 5 `pAnd`
- Haxl.Core.Monad: newMemo :: GenHaxl u (MemoVar u a)
- Haxl.Core.Monad: newMemo1 :: GenHaxl u (MemoVar1 u a b)
- Haxl.Core.Monad: newMemo2 :: GenHaxl u (MemoVar2 u a b c)
- Haxl.Core.Monad: newMemoWith :: GenHaxl u a -> GenHaxl u (MemoVar u a)
- Haxl.Core.Monad: newMemoWith1 :: (a -> GenHaxl u b) -> GenHaxl u (MemoVar1 u a b)
- Haxl.Core.Monad: newMemoWith2 :: (a -> b -> GenHaxl u c) -> GenHaxl u (MemoVar2 u a b c)
- Haxl.Core.Monad: pAnd :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool
- Haxl.Core.Monad: pOr :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool
- Haxl.Core.Monad: prepareMemo :: MemoVar u a -> GenHaxl u a -> GenHaxl u ()
- Haxl.Core.Monad: prepareMemo1 :: MemoVar1 u a b -> (a -> GenHaxl u b) -> GenHaxl u ()
- Haxl.Core.Monad: prepareMemo2 :: MemoVar2 u a b c -> (a -> b -> GenHaxl u c) -> GenHaxl u ()
- Haxl.Core.Monad: runHaxl :: Env u -> GenHaxl u a -> IO a
- Haxl.Core.Monad: runMemo :: MemoVar u a -> GenHaxl u a
- Haxl.Core.Monad: runMemo1 :: (Eq a, Hashable a) => MemoVar1 u a b -> a -> GenHaxl u b
- Haxl.Core.Monad: runMemo2 :: (Eq a, Hashable a, Eq b, Hashable b) => MemoVar2 u a b c -> a -> b -> GenHaxl u c
- Haxl.Core.Monad: type ShowReq r a = (r a -> String, a -> String)
- Haxl.Core.Monad: uncachedRequest :: (DataSource u r, Show (r a)) => r a -> GenHaxl u a
- Haxl.Core.Monad: withFingerprintLabel :: Addr# -> Addr# -> GenHaxl u a -> GenHaxl u a
- Haxl.Core.Monad: withLabel :: ProfileLabel -> GenHaxl u a -> GenHaxl u a
- Haxl.Core.Types: AsyncFetch :: (IO () -> IO ()) -> PerformFetch
- Haxl.Core.Types: BlockedFetch :: (r a) -> (ResultVar a) -> BlockedFetch r
- Haxl.Core.Types: DataCache :: (HashMap TypeRep (SubCache res)) -> DataCache res
- Haxl.Core.Types: DataSourceRoundStats :: Int -> Maybe Microseconds -> Maybe Int -> Maybe Int -> DataSourceRoundStats
- Haxl.Core.Types: FetchCall :: String -> [String] -> RoundStats
- Haxl.Core.Types: Flags :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> Flags
- Haxl.Core.Types: ProfileData :: {-# UNPACK #-} !AllocCount -> HashSet ProfileLabel -> Map Round (HashMap Text Int) -> {-# UNPACK #-} !MemoHitCount -> ProfileData
- Haxl.Core.Types: ResultVar :: (MVar (Either SomeException a)) -> ResultVar a
- Haxl.Core.Types: RoundStats :: Microseconds -> Int -> HashMap Text DataSourceRoundStats -> RoundStats
- Haxl.Core.Types: Stats :: [RoundStats] -> Stats
- Haxl.Core.Types: SubCache :: (req a -> String) -> (a -> String) -> !(HashMap (req a) (res a)) -> SubCache res
- Haxl.Core.Types: SyncFetch :: (IO ()) -> PerformFetch
- Haxl.Core.Types: [caching] :: Flags -> {-# UNPACK #-} !Int
- Haxl.Core.Types: [dataSourceAllocation] :: DataSourceRoundStats -> Maybe Int
- Haxl.Core.Types: [dataSourceFailures] :: DataSourceRoundStats -> Maybe Int
- Haxl.Core.Types: [dataSourceFetches] :: DataSourceRoundStats -> Int
- Haxl.Core.Types: [dataSourceTime] :: DataSourceRoundStats -> Maybe Microseconds
- Haxl.Core.Types: [fetchReq] :: RoundStats -> String
- Haxl.Core.Types: [fetchStack] :: RoundStats -> [String]
- Haxl.Core.Types: [profileAllocs] :: ProfileData -> {-# UNPACK #-} !AllocCount
- Haxl.Core.Types: [profileDeps] :: ProfileData -> HashSet ProfileLabel
- Haxl.Core.Types: [profileFetches] :: ProfileData -> Map Round (HashMap Text Int)
- Haxl.Core.Types: [profileMemoHits] :: ProfileData -> {-# UNPACK #-} !MemoHitCount
- Haxl.Core.Types: [report] :: Flags -> {-# UNPACK #-} !Int
- Haxl.Core.Types: [roundAllocation] :: RoundStats -> Int
- Haxl.Core.Types: [roundDataSources] :: RoundStats -> HashMap Text DataSourceRoundStats
- Haxl.Core.Types: [roundTime] :: RoundStats -> Microseconds
- Haxl.Core.Types: [trace] :: Flags -> {-# UNPACK #-} !Int
- Haxl.Core.Types: asyncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
- Haxl.Core.Types: asyncFetchAcquireRelease :: IO service -> (service -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
- Haxl.Core.Types: asyncFetchWithDispatch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
- Haxl.Core.Types: class (DataSourceName req, StateKey req, ShowP req) => DataSource u req
- Haxl.Core.Types: class DataSourceName req
- Haxl.Core.Types: data BlockedFetch r
- Haxl.Core.Types: data DataSourceRoundStats
- Haxl.Core.Types: data Flags
- Haxl.Core.Types: data PerformFetch
- Haxl.Core.Types: data Profile
- Haxl.Core.Types: data ProfileData
- Haxl.Core.Types: data RoundStats
- Haxl.Core.Types: data SubCache res
- Haxl.Core.Types: dataSourceName :: DataSourceName req => req a -> Text
- Haxl.Core.Types: defaultFlags :: Flags
- Haxl.Core.Types: emptyDataCache :: DataCache res
- Haxl.Core.Types: emptyProfile :: Profile
- Haxl.Core.Types: emptyProfileData :: ProfileData
- Haxl.Core.Types: emptyStats :: Stats
- Haxl.Core.Types: except :: (Exception e) => e -> Either SomeException a
- Haxl.Core.Types: fetch :: DataSource u req => State req -> Flags -> u -> [BlockedFetch req] -> PerformFetch
- Haxl.Core.Types: ifProfiling :: Monad m => Flags -> m a -> m ()
- Haxl.Core.Types: ifReport :: Monad m => Flags -> Int -> m a -> m ()
- Haxl.Core.Types: ifTrace :: Monad m => Flags -> Int -> m a -> m ()
- Haxl.Core.Types: instance Data.Aeson.Types.ToJSON.ToJSON Haxl.Core.Types.DataSourceRoundStats
- Haxl.Core.Types: instance Data.Aeson.Types.ToJSON.ToJSON Haxl.Core.Types.RoundStats
- Haxl.Core.Types: instance Data.Aeson.Types.ToJSON.ToJSON Haxl.Core.Types.Stats
- Haxl.Core.Types: instance GHC.Show.Show Haxl.Core.Types.DataSourceRoundStats
- Haxl.Core.Types: instance GHC.Show.Show Haxl.Core.Types.ProfileData
- Haxl.Core.Types: instance GHC.Show.Show Haxl.Core.Types.RoundStats
- Haxl.Core.Types: instance GHC.Show.Show Haxl.Core.Types.Stats
- Haxl.Core.Types: newEmptyResult :: IO (ResultVar a)
- Haxl.Core.Types: newResult :: a -> IO (ResultVar a)
- Haxl.Core.Types: newtype DataCache res
- Haxl.Core.Types: newtype ResultVar a
- Haxl.Core.Types: newtype Stats
- Haxl.Core.Types: numFetches :: Stats -> Int
- Haxl.Core.Types: numRounds :: Stats -> Int
- Haxl.Core.Types: ppDataSourceRoundStats :: DataSourceRoundStats -> String
- Haxl.Core.Types: ppRoundStats :: RoundStats -> String
- Haxl.Core.Types: ppStats :: Stats -> String
- Haxl.Core.Types: profile :: Profile -> HashMap ProfileLabel ProfileData
- Haxl.Core.Types: profileCache :: Profile -> DataCache (Constant Round)
- Haxl.Core.Types: profileRound :: Profile -> Round
- Haxl.Core.Types: putFailure :: (Exception e) => ResultVar a -> e -> IO ()
- Haxl.Core.Types: putResult :: ResultVar a -> Either SomeException a -> IO ()
- Haxl.Core.Types: putSuccess :: ResultVar a -> a -> IO ()
- Haxl.Core.Types: setError :: (Exception e) => (forall a. r a -> e) -> BlockedFetch r -> IO ()
- Haxl.Core.Types: stubFetch :: (Exception e) => (forall a. r a -> e) -> State r -> Flags -> u -> [BlockedFetch r] -> PerformFetch
- Haxl.Core.Types: syncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
- Haxl.Core.Types: takeResult :: ResultVar a -> IO (Either SomeException a)
- Haxl.Core.Types: tryReadResult :: ResultVar a -> IO (Maybe (Either SomeException a))
- Haxl.Core.Types: tryTakeResult :: ResultVar a -> IO (Maybe (Either SomeException a))
- Haxl.Core.Types: type AllocCount = Int64
- Haxl.Core.Types: type MemoHitCount = Int64
- Haxl.Core.Types: type Microseconds = Int
- Haxl.Core.Types: type ProfileLabel = Text
- Haxl.Core.Types: type Request req a = (Eq (req a), Hashable (req a), Typeable (req a), Show (req a), Show a)
- Haxl.Core.Types: type Round = Int
+ Haxl.Core: BackgroundFetch :: ([BlockedFetch req] -> IO ()) -> PerformFetch req
+ Haxl.Core: FetchStats :: Text -> {-# UNPACK #-} !Int -> !Timestamp -> {-# UNPACK #-} !Microseconds -> {-# UNPACK #-} !Int64 -> {-# UNPACK #-} !Int -> FetchStats
+ Haxl.Core: FutureFetch :: ([BlockedFetch req] -> IO (IO ())) -> PerformFetch req
+ Haxl.Core: SubmitImmediately :: SchedulerHint
+ Haxl.Core: TryToBatch :: SchedulerHint
+ Haxl.Core: [completions] :: Env u -> {-# UNPACK #-} !(TVar [CompleteReq u])
+ Haxl.Core: [fetchBatchSize] :: FetchStats -> {-# UNPACK #-} !Int
+ Haxl.Core: [fetchDataSource] :: FetchStats -> Text
+ Haxl.Core: [fetchDuration] :: FetchStats -> {-# UNPACK #-} !Microseconds
+ Haxl.Core: [fetchFailures] :: FetchStats -> {-# UNPACK #-} !Int
+ Haxl.Core: [fetchSpace] :: FetchStats -> {-# UNPACK #-} !Int64
+ Haxl.Core: [fetchStart] :: FetchStats -> !Timestamp
+ Haxl.Core: [pendingWaits] :: Env u -> [IO ()]
+ Haxl.Core: [recording] :: Flags -> {-# UNPACK #-} !Int
+ Haxl.Core: [reqStoreRef] :: Env u -> {-# UNPACK #-} !(IORef (RequestStore u))
+ Haxl.Core: [runQueueRef] :: Env u -> {-# UNPACK #-} !(IORef (JobList u))
+ Haxl.Core: [speculative] :: Env u -> {-# UNPACK #-} !Int
+ Haxl.Core: data FetchStats
+ Haxl.Core: data SchedulerHint (req :: * -> *)
+ Haxl.Core: getStateType :: StateKey f => Proxy f -> TypeRep
+ Haxl.Core: mkResultVar :: (Either SomeException a -> Bool -> IO ()) -> ResultVar a
+ Haxl.Core: newMemo :: GenHaxl u (MemoVar u a)
+ Haxl.Core: newMemoWith :: GenHaxl u a -> GenHaxl u (MemoVar u a)
+ Haxl.Core: ppFetchStats :: FetchStats -> String
+ Haxl.Core: preCacheComputation :: forall req u a. (Eq (req a), Hashable (req a), Typeable (req a)) => req a -> GenHaxl u a -> GenHaxl u a
+ Haxl.Core: prepareMemo :: MemoVar u a -> GenHaxl u a -> GenHaxl u ()
+ Haxl.Core: putResultFromChildThread :: ResultVar a -> Either SomeException a -> IO ()
+ Haxl.Core: runMemo :: MemoVar u a -> GenHaxl u a
+ Haxl.Core: schedulerHint :: DataSource u req => u -> SchedulerHint req
+ Haxl.Core: type AllocCount = Int64
+ Haxl.Core: type MemoHitCount = Int64
+ Haxl.Core: type Timestamp = Microseconds
+ Haxl.Core.DataSource: AsyncFetch :: ([BlockedFetch req] -> IO () -> IO ()) -> PerformFetch req
+ Haxl.Core.DataSource: BackgroundFetch :: ([BlockedFetch req] -> IO ()) -> PerformFetch req
+ Haxl.Core.DataSource: BlockedFetch :: (r a) -> (ResultVar a) -> BlockedFetch r
+ Haxl.Core.DataSource: FutureFetch :: ([BlockedFetch req] -> IO (IO ())) -> PerformFetch req
+ Haxl.Core.DataSource: ResultVar :: (Either SomeException a -> Bool -> IO ()) -> ResultVar a
+ Haxl.Core.DataSource: SubmitImmediately :: SchedulerHint
+ Haxl.Core.DataSource: SyncFetch :: ([BlockedFetch req] -> IO ()) -> PerformFetch req
+ Haxl.Core.DataSource: TryToBatch :: SchedulerHint
+ Haxl.Core.DataSource: asyncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
+ Haxl.Core.DataSource: asyncFetchAcquireRelease :: IO service -> (service -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
+ Haxl.Core.DataSource: asyncFetchWithDispatch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
+ Haxl.Core.DataSource: class (DataSourceName req, StateKey req, ShowP req) => DataSource u req
+ Haxl.Core.DataSource: class DataSourceName (req :: * -> *)
+ Haxl.Core.DataSource: data BlockedFetch r
+ Haxl.Core.DataSource: data PerformFetch req
+ Haxl.Core.DataSource: data SchedulerHint (req :: * -> *)
+ Haxl.Core.DataSource: dataSourceName :: DataSourceName req => Proxy req -> Text
+ Haxl.Core.DataSource: except :: (Exception e) => e -> Either SomeException a
+ Haxl.Core.DataSource: fetch :: DataSource u req => State req -> Flags -> u -> PerformFetch req
+ Haxl.Core.DataSource: mkResultVar :: (Either SomeException a -> Bool -> IO ()) -> ResultVar a
+ Haxl.Core.DataSource: newtype ResultVar a
+ Haxl.Core.DataSource: putFailure :: (Exception e) => ResultVar a -> e -> IO ()
+ Haxl.Core.DataSource: putResult :: ResultVar a -> Either SomeException a -> IO ()
+ Haxl.Core.DataSource: putResultFromChildThread :: ResultVar a -> Either SomeException a -> IO ()
+ Haxl.Core.DataSource: putSuccess :: ResultVar a -> a -> IO ()
+ Haxl.Core.DataSource: schedulerHint :: DataSource u req => u -> SchedulerHint req
+ Haxl.Core.DataSource: setError :: (Exception e) => (forall a. r a -> e) -> BlockedFetch r -> IO ()
+ Haxl.Core.DataSource: stubFetch :: (Exception e) => (forall a. r a -> e) -> State r -> Flags -> u -> PerformFetch r
+ Haxl.Core.DataSource: syncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
+ Haxl.Core.DataSource: type Request req a = (Eq (req a), Hashable (req a), Typeable (req a), Show (req a), Show a)
+ Haxl.Core.Exception: NonHaxlException :: Text -> NonHaxlException
+ Haxl.Core.Exception: newtype NonHaxlException
+ Haxl.Core.Fetch: cacheRequest :: Request req a => req a -> Either SomeException a -> GenHaxl u ()
+ Haxl.Core.Fetch: cacheResult :: Request r a => r a -> IO a -> GenHaxl u a
+ Haxl.Core.Fetch: cacheResultWithShow :: (Eq (r a), Hashable (r a), Typeable (r a)) => ShowReq r a -> r a -> IO a -> GenHaxl u a
+ Haxl.Core.Fetch: dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a
+ Haxl.Core.Fetch: dataFetchWithShow :: (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a)) => ShowReq r a -> r a -> GenHaxl u a
+ Haxl.Core.Fetch: performFetches :: forall u. Int -> Env u -> [BlockedFetches u] -> IO (Int, [IO ()])
+ Haxl.Core.Fetch: performRequestStore :: forall u. Int -> Env u -> RequestStore u -> IO (Int, [IO ()])
+ Haxl.Core.Fetch: type ShowReq r a = (r a -> String, a -> String)
+ Haxl.Core.Fetch: uncachedRequest :: (DataSource u r, Request r a) => r a -> GenHaxl u a
+ Haxl.Core.Flags: Flags :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> Flags
+ Haxl.Core.Flags: [caching] :: Flags -> {-# UNPACK #-} !Int
+ Haxl.Core.Flags: [recording] :: Flags -> {-# UNPACK #-} !Int
+ Haxl.Core.Flags: [report] :: Flags -> {-# UNPACK #-} !Int
+ Haxl.Core.Flags: [trace] :: Flags -> {-# UNPACK #-} !Int
+ Haxl.Core.Flags: data Flags
+ Haxl.Core.Flags: defaultFlags :: Flags
+ Haxl.Core.Flags: ifProfiling :: Monad m => Flags -> m a -> m ()
+ Haxl.Core.Flags: ifReport :: Monad m => Flags -> Int -> m a -> m ()
+ Haxl.Core.Flags: ifTrace :: Monad m => Flags -> Int -> m a -> m ()
+ Haxl.Core.Memo: cachedComputation :: forall req u a. (Eq (req a), Hashable (req a), Typeable (req a)) => req a -> GenHaxl u a -> GenHaxl u a
+ Haxl.Core.Memo: newMemo :: GenHaxl u (MemoVar u a)
+ Haxl.Core.Memo: newMemoWith :: GenHaxl u a -> GenHaxl u (MemoVar u a)
+ Haxl.Core.Memo: preCacheComputation :: forall req u a. (Eq (req a), Hashable (req a), Typeable (req a)) => req a -> GenHaxl u a -> GenHaxl u a
+ Haxl.Core.Memo: prepareMemo :: MemoVar u a -> GenHaxl u a -> GenHaxl u ()
+ Haxl.Core.Memo: runMemo :: MemoVar u a -> GenHaxl u a
+ Haxl.Core.Monad: (:<$>) :: (b -> a) -> (Cont u b) -> Cont u a
+ Haxl.Core.Monad: (:>>=) :: Cont u b -> (b -> GenHaxl u a) -> Cont u a
+ Haxl.Core.Monad: Blocked :: {-# UNPACK #-} !(IVar u b) -> (Cont u a) -> Result u a
+ Haxl.Core.Monad: CompleteReq :: (Either SomeException a) -> !(IVar u a) -> {-# UNPACK #-} !Int64 -> CompleteReq u
+ Haxl.Core.Monad: Cont :: (GenHaxl u a) -> Cont u a
+ Haxl.Core.Monad: Done :: a -> Result u a
+ Haxl.Core.Monad: IVar :: (IORef (IVarContents u a)) -> IVar u a
+ Haxl.Core.Monad: IVarEmpty :: (JobList u) -> IVarContents u a
+ Haxl.Core.Monad: IVarFull :: (ResultVal a) -> IVarContents u a
+ Haxl.Core.Monad: JobCons :: (Env u) -> (GenHaxl u a) -> {-# UNPACK #-} !(IVar u a) -> (JobList u) -> JobList u
+ Haxl.Core.Monad: JobNil :: JobList u
+ Haxl.Core.Monad: Ok :: a -> ResultVal a
+ Haxl.Core.Monad: Throw :: SomeException -> Result u a
+ Haxl.Core.Monad: ThrowHaxl :: SomeException -> ResultVal a
+ Haxl.Core.Monad: ThrowIO :: SomeException -> ResultVal a
+ Haxl.Core.Monad: [completions] :: Env u -> {-# UNPACK #-} !(TVar [CompleteReq u])
+ Haxl.Core.Monad: [pendingWaits] :: Env u -> [IO ()]
+ Haxl.Core.Monad: [reqStoreRef] :: Env u -> {-# UNPACK #-} !(IORef (RequestStore u))
+ Haxl.Core.Monad: [runQueueRef] :: Env u -> {-# UNPACK #-} !(IORef (JobList u))
+ Haxl.Core.Monad: [speculative] :: Env u -> {-# UNPACK #-} !Int
+ Haxl.Core.Monad: addJob :: Env u -> GenHaxl u b -> IVar u b -> IVar u a -> IO ()
+ Haxl.Core.Monad: appendJobList :: JobList u -> JobList u -> JobList u
+ Haxl.Core.Monad: data CompleteReq u
+ Haxl.Core.Monad: data Cont u a
+ Haxl.Core.Monad: data IVarContents u a
+ Haxl.Core.Monad: data JobList u
+ Haxl.Core.Monad: data Result u a
+ Haxl.Core.Monad: data ResultVal a
+ Haxl.Core.Monad: done :: ResultVal a -> IO (Result u a)
+ Haxl.Core.Monad: eitherToResult :: Either SomeException a -> ResultVal a
+ Haxl.Core.Monad: eitherToResultThrowIO :: Either SomeException a -> ResultVal a
+ Haxl.Core.Monad: getIVar :: IVar u a -> GenHaxl u a
+ Haxl.Core.Monad: imperative :: Env u -> Env u
+ Haxl.Core.Monad: lengthJobList :: JobList u -> Int
+ Haxl.Core.Monad: newFullIVar :: ResultVal a -> IO (IVar u a)
+ Haxl.Core.Monad: newIVar :: IO (IVar u a)
+ Haxl.Core.Monad: newtype IVar u a
+ Haxl.Core.Monad: putIVar :: IVar u a -> ResultVal a -> Env u -> IO ()
+ Haxl.Core.Monad: raise :: (Exception e) => e -> IO (Result u a)
+ Haxl.Core.Monad: speculate :: Env u -> Env u
+ Haxl.Core.Monad: toHaxl :: Cont u a -> GenHaxl u a
+ Haxl.Core.Parallel: infixr 4 `pOr`
+ Haxl.Core.Parallel: infixr 5 `pAnd`
+ Haxl.Core.Parallel: pAnd :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool
+ Haxl.Core.Parallel: pOr :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool
+ Haxl.Core.Profile: addProfileFetch :: forall r u a. (DataSourceName r, Eq (r a), Hashable (r a), Typeable (r a)) => Env u -> r a -> IO ()
+ Haxl.Core.Profile: collectProfileData :: ProfileLabel -> (Env u -> IO (Result u a)) -> Env u -> IO (Result u a)
+ Haxl.Core.Profile: incrementMemoHitCounterFor :: ProfileLabel -> Profile -> Profile
+ Haxl.Core.Profile: profileCont :: (Env u -> IO (Result u a)) -> Env u -> IO (Result u a)
+ Haxl.Core.Profile: withFingerprintLabel :: Addr# -> Addr# -> GenHaxl u a -> GenHaxl u a
+ Haxl.Core.Profile: withLabel :: ProfileLabel -> GenHaxl u a -> GenHaxl u a
+ Haxl.Core.RequestStore: isEmpty :: RequestStore u -> Bool
+ Haxl.Core.Run: runHaxl :: forall u a. Env u -> GenHaxl u a -> IO a
+ Haxl.Core.StateStore: getStateType :: StateKey f => Proxy f -> TypeRep
+ Haxl.Core.Stats: FetchCall :: String -> [String] -> FetchStats
+ Haxl.Core.Stats: FetchStats :: Text -> {-# UNPACK #-} !Int -> !Timestamp -> {-# UNPACK #-} !Microseconds -> {-# UNPACK #-} !Int64 -> {-# UNPACK #-} !Int -> FetchStats
+ Haxl.Core.Stats: ProfileData :: {-# UNPACK #-} !AllocCount -> HashSet ProfileLabel -> HashMap Text Int -> {-# UNPACK #-} !MemoHitCount -> ProfileData
+ Haxl.Core.Stats: Stats :: [FetchStats] -> Stats
+ Haxl.Core.Stats: [fetchBatchSize] :: FetchStats -> {-# UNPACK #-} !Int
+ Haxl.Core.Stats: [fetchDataSource] :: FetchStats -> Text
+ Haxl.Core.Stats: [fetchDuration] :: FetchStats -> {-# UNPACK #-} !Microseconds
+ Haxl.Core.Stats: [fetchFailures] :: FetchStats -> {-# UNPACK #-} !Int
+ Haxl.Core.Stats: [fetchReq] :: FetchStats -> String
+ Haxl.Core.Stats: [fetchSpace] :: FetchStats -> {-# UNPACK #-} !Int64
+ Haxl.Core.Stats: [fetchStack] :: FetchStats -> [String]
+ Haxl.Core.Stats: [fetchStart] :: FetchStats -> !Timestamp
+ Haxl.Core.Stats: [profileAllocs] :: ProfileData -> {-# UNPACK #-} !AllocCount
+ Haxl.Core.Stats: [profileDeps] :: ProfileData -> HashSet ProfileLabel
+ Haxl.Core.Stats: [profileFetches] :: ProfileData -> HashMap Text Int
+ Haxl.Core.Stats: [profileMemoHits] :: ProfileData -> {-# UNPACK #-} !MemoHitCount
+ Haxl.Core.Stats: data FetchStats
+ Haxl.Core.Stats: data Profile
+ Haxl.Core.Stats: data ProfileData
+ Haxl.Core.Stats: emptyProfile :: Profile
+ Haxl.Core.Stats: emptyProfileData :: ProfileData
+ Haxl.Core.Stats: emptyStats :: Stats
+ Haxl.Core.Stats: getAllocationCounter :: IO Int64
+ Haxl.Core.Stats: getTimestamp :: IO Timestamp
+ Haxl.Core.Stats: instance Data.Aeson.Types.ToJSON.ToJSON Haxl.Core.Stats.FetchStats
+ Haxl.Core.Stats: instance Data.Aeson.Types.ToJSON.ToJSON Haxl.Core.Stats.Stats
+ Haxl.Core.Stats: instance Data.Semigroup.Semigroup Haxl.Core.Stats.Stats
+ Haxl.Core.Stats: instance GHC.Base.Monoid Haxl.Core.Stats.Stats
+ Haxl.Core.Stats: instance GHC.Show.Show Haxl.Core.Stats.FetchStats
+ Haxl.Core.Stats: instance GHC.Show.Show Haxl.Core.Stats.ProfileData
+ Haxl.Core.Stats: instance GHC.Show.Show Haxl.Core.Stats.Stats
+ Haxl.Core.Stats: newtype Stats
+ Haxl.Core.Stats: numFetches :: Stats -> Int
+ Haxl.Core.Stats: numRounds :: Stats -> Int
+ Haxl.Core.Stats: ppFetchStats :: FetchStats -> String
+ Haxl.Core.Stats: ppStats :: Stats -> String
+ Haxl.Core.Stats: profile :: Profile -> HashMap ProfileLabel ProfileData
+ Haxl.Core.Stats: setAllocationCounter :: Int64 -> IO ()
+ Haxl.Core.Stats: type AllocCount = Int64
+ Haxl.Core.Stats: type MemoHitCount = Int64
+ Haxl.Core.Stats: type Microseconds = Int64
+ Haxl.Core.Stats: type ProfileLabel = Text
+ Haxl.Core.Stats: type Timestamp = Microseconds
+ Haxl.DataSource.ConcurrentIO: class ConcurrentIO tag where {
+ Haxl.DataSource.ConcurrentIO: data family ConcurrentIOReq tag a;
+ Haxl.DataSource.ConcurrentIO: instance (Data.Typeable.Internal.Typeable tag, Haxl.Core.ShowP.ShowP (Haxl.DataSource.ConcurrentIO.ConcurrentIOReq tag), Haxl.DataSource.ConcurrentIO.ConcurrentIO tag) => Haxl.Core.DataSource.DataSource u (Haxl.DataSource.ConcurrentIO.ConcurrentIOReq tag)
+ Haxl.DataSource.ConcurrentIO: instance Data.Typeable.Internal.Typeable tag => Haxl.Core.DataSource.DataSourceName (Haxl.DataSource.ConcurrentIO.ConcurrentIOReq tag)
+ Haxl.DataSource.ConcurrentIO: instance Data.Typeable.Internal.Typeable tag => Haxl.Core.StateStore.StateKey (Haxl.DataSource.ConcurrentIO.ConcurrentIOReq tag)
+ Haxl.DataSource.ConcurrentIO: mkConcurrentIOState :: IO (State (ConcurrentIOReq ()))
+ Haxl.DataSource.ConcurrentIO: performIO :: ConcurrentIO tag => ConcurrentIOReq tag a -> IO a
+ Haxl.DataSource.ConcurrentIO: }
+ Haxl.Prelude: liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
- Haxl.Core: AsyncFetch :: (IO () -> IO ()) -> PerformFetch
+ Haxl.Core: AsyncFetch :: ([BlockedFetch req] -> IO () -> IO ()) -> PerformFetch req
- Haxl.Core: Env :: {-# UNPACK #-} !(IORef (DataCache ResultVar)) -> {-# UNPACK #-} !(IORef (DataCache (MemoVar u))) -> !Flags -> u -> {-# UNPACK #-} !(IORef Stats) -> ProfileLabel -> {-# UNPACK #-} !(IORef Profile) -> StateStore -> Env u
+ Haxl.Core: Env :: {-# UNPACK #-} !(IORef (DataCache (IVar u))) -> {-# UNPACK #-} !(IORef (DataCache (IVar u))) -> !Flags -> u -> {-# UNPACK #-} !(IORef Stats) -> ProfileLabel -> {-# UNPACK #-} !(IORef Profile) -> StateStore -> {-# UNPACK #-} !(IORef (RequestStore u)) -> {-# UNPACK #-} !(IORef (JobList u)) -> {-# UNPACK #-} !(TVar [CompleteReq u]) -> [IO ()] -> {-# UNPACK #-} !Int -> Env u
- Haxl.Core: FetchCall :: String -> [String] -> RoundStats
+ Haxl.Core: FetchCall :: String -> [String] -> FetchStats
- Haxl.Core: Flags :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> Flags
+ Haxl.Core: Flags :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> {-# UNPACK #-} !Int -> Flags
- Haxl.Core: GenHaxl :: (Env u -> IORef (RequestStore u) -> IO (Result u a)) -> GenHaxl u a
+ Haxl.Core: GenHaxl :: (Env u -> IO (Result u a)) -> GenHaxl u a
- Haxl.Core: ProfileData :: {-# UNPACK #-} !AllocCount -> HashSet ProfileLabel -> Map Round (HashMap Text Int) -> {-# UNPACK #-} !MemoHitCount -> ProfileData
+ Haxl.Core: ProfileData :: {-# UNPACK #-} !AllocCount -> HashSet ProfileLabel -> HashMap Text Int -> {-# UNPACK #-} !MemoHitCount -> ProfileData
- Haxl.Core: ResultVar :: (MVar (Either SomeException a)) -> ResultVar a
+ Haxl.Core: ResultVar :: (Either SomeException a -> Bool -> IO ()) -> ResultVar a
- Haxl.Core: Stats :: [RoundStats] -> Stats
+ Haxl.Core: Stats :: [FetchStats] -> Stats
- Haxl.Core: SyncFetch :: (IO ()) -> PerformFetch
+ Haxl.Core: SyncFetch :: ([BlockedFetch req] -> IO ()) -> PerformFetch req
- Haxl.Core: [cacheRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache ResultVar))
+ Haxl.Core: [cacheRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (IVar u)))
- Haxl.Core: [fetchReq] :: RoundStats -> String
+ Haxl.Core: [fetchReq] :: FetchStats -> String
- Haxl.Core: [fetchStack] :: RoundStats -> [String]
+ Haxl.Core: [fetchStack] :: FetchStats -> [String]
- Haxl.Core: [memoRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (MemoVar u)))
+ Haxl.Core: [memoRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (IVar u)))
- Haxl.Core: [profileFetches] :: ProfileData -> Map Round (HashMap Text Int)
+ Haxl.Core: [profileFetches] :: ProfileData -> HashMap Text Int
- Haxl.Core: [unHaxl] :: GenHaxl u a -> Env u -> IORef (RequestStore u) -> IO (Result u a)
+ Haxl.Core: [unHaxl] :: GenHaxl u a -> Env u -> IO (Result u a)
- Haxl.Core: asyncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
+ Haxl.Core: asyncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
- Haxl.Core: asyncFetchAcquireRelease :: IO service -> (service -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
+ Haxl.Core: asyncFetchAcquireRelease :: IO service -> (service -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
- Haxl.Core: asyncFetchWithDispatch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
+ Haxl.Core: asyncFetchWithDispatch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
- Haxl.Core: class DataSourceName req
+ Haxl.Core: class DataSourceName (req :: * -> *)
- Haxl.Core: class Typeable f => StateKey (f :: * -> *) where data State f where {
+ Haxl.Core: class Typeable f => StateKey (f :: * -> *) where {
- Haxl.Core: data PerformFetch
+ Haxl.Core: data PerformFetch req
- Haxl.Core: dataSourceName :: DataSourceName req => req a -> Text
+ Haxl.Core: dataSourceName :: DataSourceName req => Proxy req -> Text
- Haxl.Core: fetch :: DataSource u req => State req -> Flags -> u -> [BlockedFetch req] -> PerformFetch
+ Haxl.Core: fetch :: DataSource u req => State req -> Flags -> u -> PerformFetch req
- Haxl.Core: memoize :: GenHaxl u a -> GenHaxl u a
+ Haxl.Core: memoize :: GenHaxl u a -> GenHaxl u (GenHaxl u a)
- Haxl.Core: runHaxl :: Env u -> GenHaxl u a -> IO a
+ Haxl.Core: runHaxl :: forall u a. Env u -> GenHaxl u a -> IO a
- Haxl.Core: stubFetch :: (Exception e) => (forall a. r a -> e) -> State r -> Flags -> u -> [BlockedFetch r] -> PerformFetch
+ Haxl.Core: stubFetch :: (Exception e) => (forall a. r a -> e) -> State r -> Flags -> u -> PerformFetch r
- Haxl.Core: syncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> [BlockedFetch request] -> PerformFetch
+ Haxl.Core: syncFetch :: ((service -> IO ()) -> IO ()) -> (service -> IO ()) -> (forall a. service -> request a -> IO (IO (Either SomeException a))) -> State request -> Flags -> u -> PerformFetch request
- Haxl.Core: type Caches u = (IORef (DataCache ResultVar), IORef (DataCache (MemoVar u)))
+ Haxl.Core: type Caches u = (IORef (DataCache (IVar u)), IORef (DataCache (IVar u)))
- Haxl.Core: type Microseconds = Int
+ Haxl.Core: type Microseconds = Int64
- Haxl.Core: uncachedRequest :: (DataSource u r, Show (r a)) => r a -> GenHaxl u a
+ Haxl.Core: uncachedRequest :: (DataSource u r, Request r a) => r a -> GenHaxl u a
- Haxl.Core.DataCache: showCache :: DataCache ResultVar -> IO [(TypeRep, [(String, Either SomeException String)])]
+ Haxl.Core.DataCache: showCache :: forall res. DataCache res -> (forall a. res a -> IO (Maybe (Either SomeException a))) -> IO [(TypeRep, [(String, Either SomeException String)])]
- Haxl.Core.Memo: memoize :: GenHaxl u a -> GenHaxl u a
+ Haxl.Core.Memo: memoize :: GenHaxl u a -> GenHaxl u (GenHaxl u a)
- Haxl.Core.Monad: Env :: {-# UNPACK #-} !(IORef (DataCache ResultVar)) -> {-# UNPACK #-} !(IORef (DataCache (MemoVar u))) -> !Flags -> u -> {-# UNPACK #-} !(IORef Stats) -> ProfileLabel -> {-# UNPACK #-} !(IORef Profile) -> StateStore -> Env u
+ Haxl.Core.Monad: Env :: {-# UNPACK #-} !(IORef (DataCache (IVar u))) -> {-# UNPACK #-} !(IORef (DataCache (IVar u))) -> !Flags -> u -> {-# UNPACK #-} !(IORef Stats) -> ProfileLabel -> {-# UNPACK #-} !(IORef Profile) -> StateStore -> {-# UNPACK #-} !(IORef (RequestStore u)) -> {-# UNPACK #-} !(IORef (JobList u)) -> {-# UNPACK #-} !(TVar [CompleteReq u]) -> [IO ()] -> {-# UNPACK #-} !Int -> Env u
- Haxl.Core.Monad: GenHaxl :: (Env u -> IORef (RequestStore u) -> IO (Result u a)) -> GenHaxl u a
+ Haxl.Core.Monad: GenHaxl :: (Env u -> IO (Result u a)) -> GenHaxl u a
- Haxl.Core.Monad: [cacheRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache ResultVar))
+ Haxl.Core.Monad: [cacheRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (IVar u)))
- Haxl.Core.Monad: [memoRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (MemoVar u)))
+ Haxl.Core.Monad: [memoRef] :: Env u -> {-# UNPACK #-} !(IORef (DataCache (IVar u)))
- Haxl.Core.Monad: [unHaxl] :: GenHaxl u a -> Env u -> IORef (RequestStore u) -> IO (Result u a)
+ Haxl.Core.Monad: [unHaxl] :: GenHaxl u a -> Env u -> IO (Result u a)
- Haxl.Core.Monad: type Caches u = (IORef (DataCache ResultVar), IORef (DataCache (MemoVar u)))
+ Haxl.Core.Monad: type Caches u = (IORef (DataCache (IVar u)), IORef (DataCache (IVar u)))
- Haxl.Core.StateStore: class Typeable f => StateKey (f :: * -> *) where data State f where {
+ Haxl.Core.StateStore: class Typeable f => StateKey (f :: * -> *) where {
- Haxl.Prelude: foldl' :: Foldable t => forall b a. (b -> a -> b) -> b -> t a -> b
+ Haxl.Prelude: foldl' :: Foldable t => forall b a. () => (b -> a -> b) -> b -> t a -> b
- Haxl.Prelude: join :: Monad m => m (m a) -> m a
+ Haxl.Prelude: join :: Monad m => m m a -> m a
- Haxl.Prelude: memoize :: GenHaxl u a -> GenHaxl u a
+ Haxl.Prelude: memoize :: GenHaxl u a -> GenHaxl u (GenHaxl u a)
Files
- Haxl/Core.hs +80/−76
- Haxl/Core/DataCache.hs +38/−9
- Haxl/Core/DataSource.hs +390/−0
- Haxl/Core/Exception.hs +3/−2
- Haxl/Core/Fetch.hs +538/−0
- Haxl/Core/Flags.hs +75/−0
- Haxl/Core/Memo.hs +272/−16
- Haxl/Core/Monad.hs +761/−1257
- Haxl/Core/Parallel.hs +84/−0
- Haxl/Core/Profile.hs +158/−0
- Haxl/Core/RequestStore.hs +13/−6
- Haxl/Core/Run.hs +220/−0
- Haxl/Core/ShowP.hs +3/−2
- Haxl/Core/StateStore.hs +22/−16
- Haxl/Core/Stats.hs +209/−0
- Haxl/Core/Types.hs +0/−636
- Haxl/Core/Util.hs +3/−20
- Haxl/DataSource/ConcurrentIO.hs +83/−0
- Haxl/Prelude.hs +4/−3
- PATENTS +0/−33
- changelog.md +23/−0
- haxl.cabal +57/−14
- readme.md +9/−0
- tests/AdoTests.hs +10/−4
- tests/AllTests.hs +11/−2
- tests/BadDataSource.hs +6/−0
- tests/BatchTests.hs +41/−33
- tests/Bench.hs +9/−3
- tests/CoreTests.hs +85/−4
- tests/DataCacheTest.hs +29/−8
- tests/ExampleDataSource.hs +10/−5
- tests/FullyAsyncTest.hs +66/−0
- tests/LoadCache.hs +6/−0
- tests/MemoizationTests.hs +7/−1
- tests/MockTAO.hs +13/−4
- tests/MonadBench.hs +4/−9
- tests/ProfileTests.hs +38/−8
- tests/SleepDataSource.hs +46/−0
- tests/TestBadDataSource.hs +6/−0
- tests/TestExampleDataSource.hs +42/−9
- tests/TestMain.hs +6/−0
- tests/TestTypes.hs +6/−0
- tests/TestUtils.hs +17/−10
- tests/WorkDataSource.hs +44/−0
Haxl/Core.hs view
@@ -2,103 +2,107 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. -- | Everything needed to define data sources and to invoke the -- engine. -- module Haxl.Core (- -- * The monad and operations- GenHaxl (..), runHaxl,+ -- * The monad and operations+ GenHaxl (..), runHaxl - -- ** Env- Env(..), Caches, caches,- -- *** Operations in the monad- env, withEnv, withLabel,- -- *** Building the Env- initEnvWithData, initEnv, emptyEnv,- -- *** Building the StateStore- StateStore, stateGet, stateSet, stateEmpty,+ -- ** Env+ , Env(..), Caches, caches+ -- *** Operations in the monad+ , env, withEnv, withLabel+ -- *** Building the Env+ , initEnvWithData, initEnv, emptyEnv+ -- *** Building the StateStore+ , StateStore, stateGet, stateSet, stateEmpty - -- ** Exceptions- throw, catch, catchIf, try, tryToHaxlException,+ -- ** Exceptions+ , throw, catch, catchIf, try, tryToHaxlException - -- ** Data fetching and caching- dataFetch, uncachedRequest,- cacheRequest, cacheResult, cacheResultWithShow, cachedComputation,- dumpCacheAsHaskell,+ -- ** Data fetching and caching+ , dataFetch, uncachedRequest+ , cacheRequest, cacheResult, cacheResultWithShow+ , cachedComputation, preCacheComputation+ , dumpCacheAsHaskell - -- ** Memoization- memo, memoize, memoize1, memoize2,- memoFingerprint, MemoFingerprintKey(..),+ -- ** Memoization+ , newMemo, newMemoWith, prepareMemo, runMemo+ , memo, memoize, memoize1, memoize2+ , memoFingerprint, MemoFingerprintKey(..) - -- ** Conditionals- pAnd, pOr,+ -- ** Conditionals+ , pAnd, pOr - -- ** Statistics- Stats(..),- RoundStats(..),- DataSourceRoundStats(..),- Microseconds,- emptyStats,- numRounds,- numFetches,- ppStats,- ppRoundStats,- ppDataSourceRoundStats,- Profile,- emptyProfile,- profile,- profileRound,- profileCache,- ProfileLabel,- ProfileData(..),- emptyProfileData,+ -- ** Statistics+ , Stats(..)+ , FetchStats(..)+ , Microseconds+ , Timestamp+ , emptyStats+ , numRounds+ , numFetches+ , ppStats+ , ppFetchStats+ , Profile+ , emptyProfile+ , profile+ , ProfileLabel+ , ProfileData(..)+ , emptyProfileData+ , AllocCount+ , MemoHitCount - -- ** Tracing flags- Flags(..),- defaultFlags,- ifTrace,- ifReport,- ifProfiling,+ -- ** Tracing flags+ , Flags(..)+ , defaultFlags+ , ifTrace+ , ifReport+ , ifProfiling - -- * Building data sources- DataSource(..),- ShowP(..),- DataSourceName(..),- Request,- BlockedFetch(..),- PerformFetch(..),- StateKey(..),+ -- * Building data sources+ , DataSource(..)+ , ShowP(..)+ , DataSourceName(..)+ , Request+ , BlockedFetch(..)+ , PerformFetch(..)+ , StateKey(..)+ , SchedulerHint(..) - -- ** Result variables- ResultVar(..),- newEmptyResult,- newResult,- putFailure,- putResult,- putSuccess,- takeResult,- tryReadResult,- tryTakeResult,+ -- ** Result variables+ , ResultVar(..)+ , mkResultVar+ , putFailure+ , putResult+ , putSuccess+ , putResultFromChildThread - -- ** Default fetch implementations- asyncFetch, asyncFetchWithDispatch, asyncFetchAcquireRelease,- stubFetch,- syncFetch,+ -- ** Default fetch implementations+ , asyncFetch, asyncFetchWithDispatch, asyncFetchAcquireRelease+ , stubFetch+ , syncFetch - -- ** Utilities- except,- setError,+ -- ** Utilities+ , except+ , setError - -- * Exceptions- module Haxl.Core.Exception+ -- * Exceptions+ , module Haxl.Core.Exception ) where +import Haxl.Core.DataSource+import Haxl.Core.Flags import Haxl.Core.Memo import Haxl.Core.Monad hiding (unsafeLiftIO {- Ask nicely to get this! -})-import Haxl.Core.Types+import Haxl.Core.Fetch+import Haxl.Core.Parallel+import Haxl.Core.Profile+import Haxl.Core.Run+import Haxl.Core.Stats import Haxl.Core.Exception import Haxl.Core.ShowP (ShowP(..)) import Haxl.Core.StateStore
Haxl/Core/DataCache.hs view
@@ -2,17 +2,19 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. {-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} --- | A cache mapping data requests to their results. This module is+-- |+-- A cache mapping data requests to their results. This module is -- provided for access to Haxl internals only; most users should not -- need to import it.+-- module Haxl.Core.DataCache ( DataCache(..) , SubCache(..)@@ -24,19 +26,44 @@ , showCache ) where +import Control.Exception import Data.Hashable import Prelude hiding (lookup) import Unsafe.Coerce+import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.Typeable import Data.Maybe #if __GLASGOW_HASKELL__ < 710 import Control.Applicative ((<$>)) #endif-import Control.Exception -import Haxl.Core.Types+-- ---------------------------------------------------------------------------+-- DataCache +-- | A @'DataCache' res@ maps things of type @req a@ to @res a@, for+-- any @req@ and @a@ provided @req a@ is an instance of 'Typeable'. In+-- practice @req a@ will be a request type parameterised by its result.+--+newtype DataCache res = DataCache (HashMap TypeRep (SubCache res))++-- | The implementation is a two-level map: the outer level maps the+-- types of requests to 'SubCache', which maps actual requests to their+-- results. So each 'SubCache' contains requests of the same type.+-- This works well because we only have to store the dictionaries for+-- 'Hashable' and 'Eq' once per request type.+--+data SubCache res =+ forall req a . (Hashable (req a), Eq (req a), Typeable (req a)) =>+ SubCache (req a -> String) (a -> String) ! (HashMap (req a) (res a))+ -- NB. the inner HashMap is strict, to avoid building up+ -- a chain of thunks during repeated insertions.++-- | A new, empty 'DataCache'.+emptyDataCache :: DataCache res+emptyDataCache = DataCache HashMap.empty++ -- | Inserts a request-result pair into the 'DataCache'. insert :: (Hashable (req a), Typeable (req a), Eq (req a), Show (req a), Show a)@@ -109,20 +136,22 @@ -- 'insertNotShowable' has been used to insert any entries. -- showCache- :: DataCache ResultVar+ :: forall res+ . DataCache res+ -> (forall a . res a -> IO (Maybe (Either SomeException a))) -> IO [(TypeRep, [(String, Either SomeException String)])] -showCache (DataCache cache) = mapM goSubCache (HashMap.toList cache)+showCache (DataCache cache) readRes = mapM goSubCache (HashMap.toList cache) where goSubCache- :: (TypeRep,SubCache ResultVar)+ :: (TypeRep, SubCache res) -> IO (TypeRep,[(String, Either SomeException String)]) goSubCache (ty, SubCache showReq showRes hmap) = do elems <- catMaybes <$> mapM go (HashMap.toList hmap) return (ty, elems) where go (req, rvar) = do- maybe_r <- tryReadResult rvar+ maybe_r <- readRes rvar case maybe_r of Nothing -> return Nothing Just (Left e) -> return (Just (showReq req, Left e))
+ Haxl/Core/DataSource.hs view
@@ -0,0 +1,390 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- The 'DataSource' class and related types and functions. This+-- module is provided for access to Haxl internals only; most users+-- should import "Haxl.Core" instead.+--+module Haxl.Core.DataSource+ (+ -- * Data fetching+ DataSource(..)+ , DataSourceName(..)+ , Request+ , BlockedFetch(..)+ , PerformFetch(..)+ , SchedulerHint(..)++ -- * Result variables+ , ResultVar(..)+ , mkResultVar+ , putFailure+ , putResult+ , putResultFromChildThread+ , putSuccess++ -- * Default fetch implementations+ , asyncFetch, asyncFetchWithDispatch+ , asyncFetchAcquireRelease+ , stubFetch+ , syncFetch++ -- * Utilities+ , except+ , setError+ ) where++#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif+import Control.Exception+import Data.Hashable+import Data.Text (Text)+#if __GLASGOW_HASKELL__ >= 802+import Data.Typeable+#else+import Data.Typeable.Internal+#endif++import Haxl.Core.Exception+import Haxl.Core.Flags+import Haxl.Core.ShowP+import Haxl.Core.StateStore+++-- ---------------------------------------------------------------------------+-- DataSource class++-- | The class of data sources, parameterised over the request type for+-- that data source. Every data source must implement this class.+--+-- A data source keeps track of its state by creating an instance of+-- 'StateKey' to map the request type to its state. In this case, the+-- type of the state should probably be a reference type of some kind,+-- such as 'IORef'.+--+-- For a complete example data source, see+-- <https://github.com/facebook/Haxl/tree/master/example Examples>.+--+class (DataSourceName req, StateKey req, ShowP req) => DataSource u req where++ -- | Issues a list of fetches to this 'DataSource'. The 'BlockedFetch'+ -- objects contain both the request and the 'ResultVar's into which to put+ -- the results.+ fetch+ :: State req+ -- ^ Current state.+ -> Flags+ -- ^ Tracing flags.+ -> u+ -- ^ User environment.+ -> PerformFetch req+ -- ^ Fetch the data; see 'PerformFetch'.++ schedulerHint :: u -> SchedulerHint req+ schedulerHint _ = TryToBatch++class DataSourceName (req :: * -> *) where+ -- | The name of this 'DataSource', used in tracing and stats. Must+ -- take a dummy request.+ dataSourceName :: Proxy req -> Text++-- The 'ShowP' class is a workaround for the fact that we can't write+-- @'Show' (req a)@ as a superclass of 'DataSource', without also+-- parameterizing 'DataSource' over @a@, which is a pain (I tried+-- it). 'ShowP' seems fairly benign, though.++-- | A convenience only: package up 'Eq', 'Hashable', 'Typeable', and 'Show'+-- for requests into a single constraint.+type Request req a =+ ( Eq (req a)+ , Hashable (req a)+ , Typeable (req a)+ , Show (req a)+ , Show a+ )++-- | Hints to the scheduler about this data source+data SchedulerHint (req :: * -> *)+ = TryToBatch+ -- ^ Hold data-source requests while we execute as much as we can, so+ -- that we can hopefully collect more requests to batch.+ | SubmitImmediately+ -- ^ Submit a request via fetch as soon as we have one, don't try to+ -- batch multiple requests. This is really only useful if the data source+ -- returns BackgroundFetch, otherwise requests to this data source will+ -- be performed synchronously, one at a time.++-- | A data source can fetch data in one of four ways.+--+data PerformFetch req+ = SyncFetch ([BlockedFetch req] -> IO ())+ -- ^ Fully synchronous, returns only when all the data is fetched.+ -- See 'syncFetch' for an example.+ | AsyncFetch ([BlockedFetch req] -> IO () -> IO ())+ -- ^ Asynchronous; performs an arbitrary IO action while the data+ -- is being fetched, but only returns when all the data is+ -- fetched. See 'asyncFetch' for an example.+ | BackgroundFetch ([BlockedFetch req] -> IO ())+ -- ^ Fetches the data in the background, calling 'putResult' at+ -- any time in the future. This is the best kind of fetch,+ -- because it provides the most concurrency.+ | FutureFetch ([BlockedFetch req] -> IO (IO ()))+ -- ^ Returns an IO action that, when performed, waits for the data+ -- to be received. This is the second-best type of fetch, because+ -- the scheduler still has to perform the blocking wait at some+ -- point in the future, and when it has multiple blocking waits to+ -- perform, it can't know which one will return first.+ --+ -- Why not just forkIO the IO action to make a FutureFetch into a+ -- BackgroundFetch? The blocking wait will probably do a safe FFI+ -- call, which means it needs its own OS thread. If we don't want+ -- to create an arbitrary number of OS threads, then FutureFetch+ -- enables all the blocking waits to be done on a single thread.+ -- Also, you might have a data source that requires all calls to+ -- be made in the same OS thread.+++-- | A 'BlockedFetch' is a pair of+--+-- * The request to fetch (with result type @a@)+--+-- * A 'ResultVar' to store either the result or an error+--+-- We often want to collect together multiple requests, but they return+-- different types, and the type system wouldn't let us put them+-- together in a list because all the elements of the list must have the+-- same type. So we wrap up these types inside the 'BlockedFetch' type,+-- so that they all look the same and we can put them in a list.+--+-- When we unpack the 'BlockedFetch' and get the request and the 'ResultVar'+-- out, the type system knows that the result type of the request+-- matches the type parameter of the 'ResultVar', so it will let us take the+-- result of the request and store it in the 'ResultVar'.+--+data BlockedFetch r = forall a. BlockedFetch (r a) (ResultVar a)+++-- -----------------------------------------------------------------------------+-- ResultVar++-- | A sink for the result of a data fetch in 'BlockedFetch'+newtype ResultVar a = ResultVar (Either SomeException a -> Bool -> IO ())+ -- The Bool here is True if result was returned by a child thread,+ -- rather than the main runHaxl thread. see Note [tracking allocation in+ -- child threads]++mkResultVar :: (Either SomeException a -> Bool -> IO ()) -> ResultVar a+mkResultVar = ResultVar++putFailure :: (Exception e) => ResultVar a -> e -> IO ()+putFailure r = putResult r . except++putSuccess :: ResultVar a -> a -> IO ()+putSuccess r = putResult r . Right++putResult :: ResultVar a -> Either SomeException a -> IO ()+putResult (ResultVar io) res = io res False++-- | Like `putResult`, but used to get correct accounting when work is+-- being done in child threads. This is particularly important for+-- data sources that are using 'BackgroundFetch', The allocation performed+-- in the child thread up to this point will be propagated back to the+-- thread that called 'runHaxl'.+--+-- Note: if you're doing multiple 'putResult' calls in the same thread+-- ensure that only the /last/ one is 'putResultFromChildThread'. If you+-- make multiple 'putResultFromChildThread' calls, the allocation will be+-- counted multiple times.+--+-- If you are reusing a thread for multiple fetches, you should call+-- @System.Mem.setAllocationCounter 0@ after+-- 'putResultFromChildThread', so that allocation is not counted+-- multiple times.+putResultFromChildThread :: ResultVar a -> Either SomeException a -> IO ()+putResultFromChildThread (ResultVar io) res = io res True+ -- see Note [tracking allocation in child threads]++-- | Function for easily setting a fetch to a particular exception+setError :: (Exception e) => (forall a. r a -> e) -> BlockedFetch r -> IO ()+setError e (BlockedFetch req m) = putFailure m (e req)++except :: (Exception e) => e -> Either SomeException a+except = Left . toException+++-- -----------------------------------------------------------------------------+-- Fetch templates++stubFetch+ :: (Exception e) => (forall a. r a -> e)+ -> State r -> Flags -> u -> PerformFetch r+stubFetch e _state _flags _si = SyncFetch $ mapM_ (setError e)++-- | Common implementation templates for 'fetch' of 'DataSource'.+--+-- Example usage:+--+-- > fetch = syncFetch MyDS.withService MyDS.retrieve+-- > $ \service request -> case request of+-- > This x -> MyDS.fetchThis service x+-- > That y -> MyDS.fetchThat service y+--+asyncFetchWithDispatch+ :: ((service -> IO ()) -> IO ())+ -- ^ Wrapper to perform an action in the context of a service.++ -> (service -> IO ())+ -- ^ Dispatch all the pending requests++ -> (service -> IO ())+ -- ^ Wait for the results++ -> (forall a. service -> request a -> IO (IO (Either SomeException a)))+ -- ^ Enqueue an individual request to the service.++ -> State request+ -- ^ Currently unused.++ -> Flags+ -- ^ Currently unused.++ -> u+ -- ^ Currently unused.++ -> PerformFetch request++asyncFetch, syncFetch+ :: ((service -> IO ()) -> IO ())+ -- ^ Wrapper to perform an action in the context of a service.++ -> (service -> IO ())+ -- ^ Dispatch all the pending requests and wait for the results++ -> (forall a. service -> request a -> IO (IO (Either SomeException a)))+ -- ^ Submits an individual request to the service.++ -> State request+ -- ^ Currently unused.++ -> Flags+ -- ^ Currently unused.++ -> u+ -- ^ Currently unused.++ -> PerformFetch request++asyncFetchWithDispatch+ withService dispatch wait enqueue _state _flags _si =+ AsyncFetch $ \requests inner -> withService $ \service -> do+ getResults <- mapM (submitFetch service enqueue) requests+ dispatch service+ inner+ wait service+ sequence_ getResults++asyncFetch withService wait enqueue _state _flags _si =+ AsyncFetch $ \requests inner -> withService $ \service -> do+ getResults <- mapM (submitFetch service enqueue) requests+ inner+ wait service+ sequence_ getResults++syncFetch withService dispatch enqueue _state _flags _si =+ SyncFetch $ \requests -> withService $ \service -> do+ getResults <- mapM (submitFetch service enqueue) requests+ dispatch service+ sequence_ getResults+++{- |+A version of 'asyncFetch' (actually 'asyncFetchWithDispatch') that+handles exceptions correctly. You should use this instead of+'asyncFetch' or 'asyncFetchWithDispatch'. The danger with+'asyncFetch' is that if an exception is thrown by @withService@, the+@inner@ action won't be executed, and we'll drop some data-fetches in+the same round.++'asyncFetchAcquireRelease' behaves like the following:++> asyncFetchAcquireRelease acquire release dispatch wait enqueue =+> AsyncFetch $ \requests inner ->+> bracket acquire release $ \service -> do+> getResults <- mapM (submitFetch service enqueue) requests+> dispatch service+> inner+> wait service+> sequence_ getResults++except that @inner@ is run even if @acquire@, @enqueue@, or @dispatch@ throws,+/unless/ an async exception is received.+-}++asyncFetchAcquireRelease+ :: IO service+ -- ^ Resource acquisition for this datasource++ -> (service -> IO ())+ -- ^ Resource release++ -> (service -> IO ())+ -- ^ Dispatch all the pending requests and wait for the results++ -> (service -> IO ())+ -- ^ Wait for the results++ -> (forall a. service -> request a -> IO (IO (Either SomeException a)))+ -- ^ Submits an individual request to the service.++ -> State request+ -- ^ Currently unused.++ -> Flags+ -- ^ Currently unused.++ -> u+ -- ^ Currently unused.++ -> PerformFetch request++asyncFetchAcquireRelease+ acquire release dispatch wait enqueue _state _flags _si =+ AsyncFetch $ \requests inner -> mask $ \restore -> do+ r1 <- tryWithRethrow acquire+ case r1 of+ Left err -> do restore inner; throwIO (err :: SomeException)+ Right service -> do+ flip finally (release service) $ restore $ do+ r2 <- tryWithRethrow $ do+ getResults <- mapM (submitFetch service enqueue) requests+ dispatch service+ return getResults+ inner -- we assume this cannot throw, ensured by performFetches+ case r2 of+ Left err -> throwIO (err :: SomeException)+ Right getResults -> do wait service; sequence_ getResults++-- | Used by 'asyncFetch' and 'syncFetch' to retrieve the results of+-- requests to a service.+submitFetch+ :: service+ -> (forall a. service -> request a -> IO (IO (Either SomeException a)))+ -> BlockedFetch request+ -> IO (IO ())+submitFetch service fetch (BlockedFetch request result)+ = (putResult result =<<) <$> fetch service request
Haxl/Core/Exception.hs view
@@ -2,8 +2,7 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. {-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -29,6 +28,7 @@ -- -- Most users should import "Haxl.Core" instead of importing this -- module directly.+-- module Haxl.Core.Exception ( HaxlException(..),@@ -53,6 +53,7 @@ -- ** Internal exceptions CriticalError(..), DataSourceError(..),+ NonHaxlException(..), -- ** Logic exceptions NotFound(..),
+ Haxl/Core/Fetch.hs view
@@ -0,0 +1,538 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Implementation of data-fetching operations. Most users should+-- import "Haxl.Core" instead.+--+module Haxl.Core.Fetch+ ( dataFetch+ , dataFetchWithShow+ , uncachedRequest+ , cacheResult+ , cacheResultWithShow+ , cacheRequest+ , performFetches+ , performRequestStore+ , ShowReq+ ) where++import Control.Concurrent.STM+import Control.Exception as Exception+import Control.Monad+import Data.Either+import Data.Hashable+import Data.IORef+import Data.Int+import Data.List+import Data.Monoid+import Data.Typeable+import Data.Text (Text)+import qualified Data.Text as Text+import Text.Printf+#ifdef PROFILING+import GHC.Stack+#endif++import Haxl.Core.DataSource+import Haxl.Core.DataCache as DataCache+import Haxl.Core.Exception+import Haxl.Core.Flags+import Haxl.Core.Monad+import Haxl.Core.Profile+import Haxl.Core.RequestStore+import Haxl.Core.ShowP+import Haxl.Core.Stats+import Haxl.Core.StateStore+import Haxl.Core.Util++-- -----------------------------------------------------------------------------+-- Data fetching and caching++-- | Possible responses when checking the cache.+data CacheResult u a+ -- | The request hadn't been seen until now.+ = Uncached+ (ResultVar a)+ {-# UNPACK #-} !(IVar u a)++ -- | The request has been seen before, but its result has not yet been+ -- fetched.+ | CachedNotFetched+ {-# UNPACK #-} !(IVar u a)++ -- | The request has been seen before, and its result has already been+ -- fetched.+ | Cached (ResultVal a)+++-- | Show functions for request and its result.+type ShowReq r a = (r a -> String, a -> String)++-- Note [showFn]+--+-- Occasionally, for tracing purposes or generating exceptions, we need to+-- call 'show' on the request in a place where we *cannot* have a Show+-- dictionary. (Because the function is a worker which is called by one of+-- the *WithShow variants that take explicit show functions via a ShowReq+-- argument.) None of the functions that does this is exported, so this is+-- hidden from the Haxl user.++cachedWithInsert+ :: forall r a u .+ Typeable (r a)+ => (r a -> String) -- See Note [showFn]+ -> (r a -> IVar u a -> DataCache (IVar u) -> DataCache (IVar u))+ -> Env u -> r a -> IO (CacheResult u a)+cachedWithInsert showFn insertFn Env{..} req = do+ cache <- readIORef cacheRef+ let+ doFetch = do+ ivar <- newIVar+ let !rvar = stdResultVar ivar completions+ writeIORef cacheRef $! insertFn req ivar cache+ return (Uncached rvar ivar)+ case DataCache.lookup req cache of+ Nothing -> doFetch+ Just (IVar cr) -> do+ e <- readIORef cr+ case e of+ IVarEmpty _ -> return (CachedNotFetched (IVar cr))+ IVarFull r -> do+ ifTrace flags 3 $ putStrLn $ case r of+ ThrowIO _ -> "Cached error: " ++ showFn req+ ThrowHaxl _ -> "Cached error: " ++ showFn req+ Ok _ -> "Cached request: " ++ showFn req+ return (Cached r)+++-- | Make a ResultVar with the standard function for sending a CompletionReq+-- to the scheduler.+stdResultVar :: IVar u a -> TVar [CompleteReq u] -> ResultVar a+stdResultVar ivar completions = mkResultVar $ \r isChildThread -> do+ allocs <- if isChildThread+ then+ -- In a child thread, return the current allocation counter too,+ -- for correct tracking of allocation.+ getAllocationCounter+ else+ return 0+ atomically $ do+ cs <- readTVar completions+ writeTVar completions (CompleteReq r ivar allocs : cs)+{-# INLINE stdResultVar #-}+++-- | Record the call stack for a data fetch in the Stats. Only useful+-- when profiling.+logFetch :: Env u -> (r a -> String) -> r a -> IO ()+#ifdef PROFILING+logFetch env showFn req = do+ ifReport (flags env) 5 $ do+ stack <- currentCallStack+ modifyIORef' (statsRef env) $ \(Stats s) ->+ Stats (FetchCall (showFn req) stack : s)+#else+logFetch _ _ _ = return ()+#endif++-- | Performs actual fetching of data for a 'Request' from a 'DataSource'.+dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a+dataFetch = dataFetchWithInsert show DataCache.insert++-- | Performs actual fetching of data for a 'Request' from a 'DataSource', using+-- the given show functions for requests and their results.+dataFetchWithShow+ :: (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a))+ => ShowReq r a+ -> r a -> GenHaxl u a+dataFetchWithShow (showReq, showRes) = dataFetchWithInsert showReq+ (DataCache.insertWithShow showReq showRes)++-- | Performs actual fetching of data for a 'Request' from a 'DataSource', using+-- the given function to insert requests in the cache.+dataFetchWithInsert+ :: forall u r a+ . (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a))+ => (r a -> String) -- See Note [showFn]+ -> (r a -> IVar u a -> DataCache (IVar u) -> DataCache (IVar u))+ -> r a+ -> GenHaxl u a+dataFetchWithInsert showFn insertFn req =+ GenHaxl $ \env@Env{..} -> do+ -- First, check the cache+ res <- cachedWithInsert showFn insertFn env req+ ifProfiling flags $ addProfileFetch env req+ case res of+ -- This request has not been seen before+ Uncached rvar ivar -> do+ logFetch env showFn req+ --+ -- Check whether the data source wants to submit requests+ -- eagerly, or batch them up.+ --+ case schedulerHint userEnv :: SchedulerHint r of+ SubmitImmediately -> do+ (_,ios) <- performFetches 0 env+ [BlockedFetches [BlockedFetch req rvar]]+ when (not (null ios)) $+ error "bad data source:SubmitImmediately but returns FutureFetch"+ TryToBatch ->+ -- add the request to the RequestStore and continue+ modifyIORef' reqStoreRef $ \bs ->+ addRequest (BlockedFetch req rvar) bs+ --+ return $ Blocked ivar (Cont (getIVar ivar))++ -- Seen before but not fetched yet. We're blocked, but we don't have+ -- to add the request to the RequestStore.+ CachedNotFetched ivar -> return $ Blocked ivar (Cont (getIVar ivar))++ -- Cached: either a result, or an exception+ Cached r -> done r++-- | A data request that is not cached. This is not what you want for+-- normal read requests, because then multiple identical requests may+-- return different results, and this invalidates some of the+-- properties that we expect Haxl computations to respect: that data+-- fetches can be arbitrarily reordered, and identical requests can be+-- commoned up, for example.+--+-- 'uncachedRequest' is useful for performing writes, provided those+-- are done in a safe way - that is, not mixed with reads that might+-- conflict in the same Haxl computation.+--+-- if we are recording or running a test, we fallback to using dataFetch+-- This allows us to store the request in the cache when recording, which+-- allows a transparent run afterwards. Without this, the test would try to+-- call the datasource during testing and that would be an exception.+uncachedRequest :: (DataSource u r, Request r a) => r a -> GenHaxl u a+uncachedRequest req = do+ isRecordingFlag <- env (recording . flags)+ if isRecordingFlag /= 0+ then dataFetch req+ else GenHaxl $ \Env{..} -> do+ ivar <- newIVar+ let !rvar = stdResultVar ivar completions+ modifyIORef' reqStoreRef $ \bs ->+ addRequest (BlockedFetch req rvar) bs+ return $ Blocked ivar (Cont (getIVar ivar))+++-- | Transparently provides caching. Useful for datasources that can+-- return immediately, but also caches values. Exceptions thrown by+-- the IO operation (except for asynchronous exceptions) are+-- propagated into the Haxl monad and can be caught by 'catch' and+-- 'try'.+cacheResult :: Request r a => r a -> IO a -> GenHaxl u a+cacheResult = cacheResultWithInsert show DataCache.insert++-- | Transparently provides caching in the same way as 'cacheResult', but uses+-- the given functions to show requests and their results.+cacheResultWithShow+ :: (Eq (r a), Hashable (r a), Typeable (r a))+ => ShowReq r a -> r a -> IO a -> GenHaxl u a+cacheResultWithShow (showReq, showRes) = cacheResultWithInsert showReq+ (DataCache.insertWithShow showReq showRes)++-- Transparently provides caching, using the given function to insert requests+-- into the cache.+cacheResultWithInsert+ :: Typeable (r a)+ => (r a -> String) -- See Note [showFn]+ -> (r a -> IVar u a -> DataCache (IVar u) -> DataCache (IVar u)) -> r a+ -> IO a -> GenHaxl u a+cacheResultWithInsert showFn insertFn req val = GenHaxl $ \env -> do+ let !ref = cacheRef env+ cache <- readIORef ref+ case DataCache.lookup req cache of+ Nothing -> do+ eitherResult <- Exception.try val+ case eitherResult of+ Left e -> rethrowAsyncExceptions e+ _ -> return ()+ let result = eitherToResultThrowIO eitherResult+ ivar <- newFullIVar result+ writeIORef ref $! insertFn req ivar cache+ done result+ Just (IVar cr) -> do+ e <- readIORef cr+ case e of+ IVarEmpty _ -> corruptCache+ IVarFull r -> done r+ where+ corruptCache = raise . DataSourceError $ Text.concat+ [ Text.pack (showFn req)+ , " has a corrupted cache value: these requests are meant to"+ , " return immediately without an intermediate value. Either"+ , " the cache was updated incorrectly, or you're calling"+ , " cacheResult on a query that involves a blocking fetch."+ ]++-- | Inserts a request/result pair into the cache. Throws an exception+-- if the request has already been issued, either via 'dataFetch' or+-- 'cacheRequest'.+--+-- This can be used to pre-populate the cache when running tests, to+-- avoid going to the actual data source and ensure that results are+-- deterministic.+--+cacheRequest+ :: Request req a => req a -> Either SomeException a -> GenHaxl u ()+cacheRequest request result = GenHaxl $ \env -> do+ cache <- readIORef (cacheRef env)+ case DataCache.lookup request cache of+ Nothing -> do+ cr <- newFullIVar (eitherToResult result)+ writeIORef (cacheRef env) $! DataCache.insert request cr cache+ return (Done ())++ -- It is an error if the request is already in the cache. We can't test+ -- whether the cached result is the same without adding an Eq constraint,+ -- and we don't necessarily have Eq for all results.+ _other -> raise $+ DataSourceError "cacheRequest: request is already in the cache"++performRequestStore+ :: forall u. Int -> Env u -> RequestStore u -> IO (Int, [IO ()])+performRequestStore n env reqStore =+ performFetches n env (contents reqStore)++-- | Issues a batch of fetches in a 'RequestStore'. After+-- 'performFetches', all the requests in the 'RequestStore' are+-- complete, and all of the 'ResultVar's are full.+performFetches+ :: forall u. Int -> Env u -> [BlockedFetches u] -> IO (Int, [IO ()])+performFetches n env@Env{flags=f, statsRef=sref} jobs = do+ let !n' = n + length jobs++ t0 <- getTimestamp++ let+ roundstats =+ [ (dataSourceName (Proxy :: Proxy r), length reqs)+ | BlockedFetches (reqs :: [BlockedFetch r]) <- jobs ]++ ifTrace f 1 $+ printf "Batch data fetch (%s)\n" $+ intercalate (", "::String) $+ map (\(name,num) -> printf "%d %s" num (Text.unpack name)) roundstats++ ifTrace f 3 $+ forM_ jobs $ \(BlockedFetches reqs) ->+ forM_ reqs $ \(BlockedFetch r _) -> putStrLn (showp r)++ let+ applyFetch i (BlockedFetches (reqs :: [BlockedFetch r])) =+ case stateGet (states env) of+ Nothing ->+ return (FetchToDo reqs (SyncFetch (mapM_ (setError e))))+ where+ e :: ShowP req => req a -> DataSourceError+ e req = DataSourceError $ "data source not initialized: " <> dsName+ <> ": "+ <> Text.pack (showp req)+ Just state ->+ return+ $ FetchToDo reqs+ $ (if report f >= 2+ then wrapFetchInStats sref dsName (length reqs)+ else id)+ $ wrapFetchInTrace i (length reqs) dsName+ $ wrapFetchInCatch reqs+ $ fetch state f (userEnv env)+ where+ dsName = dataSourceName (Proxy :: Proxy r)++ fetches <- zipWithM applyFetch [n..] jobs++ waits <- scheduleFetches fetches++ t1 <- getTimestamp+ let roundtime = fromIntegral (t1 - t0) / 1000000 :: Double++ ifTrace f 1 $+ printf "Batch data fetch done (%.2fs)\n" (realToFrac roundtime :: Double)++ return (n', waits)++data FetchToDo where+ FetchToDo :: [BlockedFetch req] -> PerformFetch req -> FetchToDo++-- Catch exceptions arising from the data source and stuff them into+-- the appropriate requests. We don't want any exceptions propagating+-- directly from the data sources, because we want the exception to be+-- thrown by dataFetch instead.+--+wrapFetchInCatch :: [BlockedFetch req] -> PerformFetch req -> PerformFetch req+wrapFetchInCatch reqs fetch =+ case fetch of+ SyncFetch f ->+ SyncFetch $ \reqs -> f reqs `Exception.catch` handler+ AsyncFetch f ->+ AsyncFetch $ \reqs io -> f reqs io `Exception.catch` handler+ -- this might be wrong: if the outer 'fio' throws an exception,+ -- then we don't know whether we have executed the inner 'io' or+ -- not. If not, then we'll likely get some errors about "did+ -- not set result var" later, because we haven't executed some+ -- data fetches. But we can't execute 'io' in the handler,+ -- because we might have already done it. It isn't possible to+ -- do it completely right here, so we have to rely on data+ -- sources themselves to catch (synchronous) exceptions. Async+ -- exceptions aren't a problem because we're going to rethrow+ -- them all the way to runHaxl anyway.+ FutureFetch f ->+ FutureFetch $ \reqs -> f reqs `Exception.catch` (+ \e -> handler e >> return (return ()))+ BackgroundFetch f ->+ BackgroundFetch $ \reqs -> f reqs `Exception.catch` handler+ where+ handler :: SomeException -> IO ()+ handler e = do+ rethrowAsyncExceptions e+ mapM_ (forceError e) reqs++ -- Set the exception even if the request already had a result.+ -- Otherwise we could be discarding an exception.+ forceError e (BlockedFetch _ rvar) =+ putResult rvar (except e)+++wrapFetchInStats+ :: IORef Stats+ -> Text+ -> Int+ -> PerformFetch req+ -> PerformFetch req++wrapFetchInStats !statsRef dataSource batchSize perform = do+ case perform of+ SyncFetch f ->+ SyncFetch $ \reqs -> do+ fail_ref <- newIORef 0+ (t0,t,alloc,_) <- statsForIO (f (map (addFailureCount fail_ref) reqs))+ failures <- readIORef fail_ref+ updateFetchStats t0 t alloc batchSize failures+ AsyncFetch f -> do+ AsyncFetch $ \reqs inner -> do+ inner_r <- newIORef (0, 0)+ fail_ref <- newIORef 0+ let inner' = do+ (_,t,alloc,_) <- statsForIO inner+ writeIORef inner_r (t,alloc)+ reqs' = map (addFailureCount fail_ref) reqs+ (t0, totalTime, totalAlloc, _) <- statsForIO (f reqs' inner')+ (innerTime, innerAlloc) <- readIORef inner_r+ failures <- readIORef fail_ref+ updateFetchStats t0 (totalTime - innerTime) (totalAlloc - innerAlloc)+ batchSize failures+ FutureFetch submit ->+ FutureFetch $ \reqs -> do+ fail_ref <- newIORef 0+ let reqs' = map (addFailureCount fail_ref) reqs+ (t0, submitTime, submitAlloc, wait) <- statsForIO (submit reqs')+ return $ do+ (_, waitTime, waitAlloc, _) <- statsForIO wait+ failures <- readIORef fail_ref+ updateFetchStats t0 (submitTime + waitTime) (submitAlloc + waitAlloc)+ batchSize failures+ BackgroundFetch io -> do+ BackgroundFetch $ \reqs -> do+ startTime <- getTimestamp+ io (map (addTimer startTime) reqs)+ where+ statsForIO io = do+ prevAlloc <- getAllocationCounter+ (t0,t,a) <- time io+ postAlloc <- getAllocationCounter+ return (t0,t, fromIntegral $ prevAlloc - postAlloc, a)++ addTimer t0 (BlockedFetch req (ResultVar fn)) =+ BlockedFetch req $ ResultVar $ \result isChildThread -> do+ t1 <- getTimestamp+ updateFetchStats t0 (t1 - t0)+ 0 -- allocs: we can't measure this easily for BackgroundFetch+ 1 -- batch size: we don't know if this is a batch or not+ (if isLeft result then 1 else 0) -- failures+ fn result isChildThread++ updateFetchStats+ :: Timestamp -> Microseconds -> Int64 -> Int -> Int -> IO ()+ updateFetchStats start time space batch failures = do+ let this = FetchStats { fetchDataSource = dataSource+ , fetchBatchSize = batch+ , fetchStart = start+ , fetchDuration = time+ , fetchSpace = space+ , fetchFailures = failures }+ atomicModifyIORef' statsRef $ \(Stats fs) -> (Stats (this : fs), ())++ addFailureCount :: IORef Int -> BlockedFetch r -> BlockedFetch r+ addFailureCount ref (BlockedFetch req (ResultVar fn)) =+ BlockedFetch req $ ResultVar $ \result isChildThread -> do+ when (isLeft result) $ atomicModifyIORef' ref (\r -> (r+1,()))+ fn result isChildThread++wrapFetchInTrace+ :: Int+ -> Int+ -> Text+ -> PerformFetch req+ -> PerformFetch req+#ifdef EVENTLOG+wrapFetchInTrace i n dsName f =+ case f of+ SyncFetch io -> SyncFetch (wrapF "Sync" io)+ AsyncFetch fio -> AsyncFetch (wrapF "Async" . fio . unwrapF "Async")+ where+ d = Text.unpack dsName+ wrapF :: String -> IO a -> IO a+ wrapF ty = bracket_ (traceEventIO $ printf "START %d %s (%d %s)" i d n ty)+ (traceEventIO $ printf "STOP %d %s (%d %s)" i d n ty)+ unwrapF :: String -> IO a -> IO a+ unwrapF ty = bracket_ (traceEventIO $ printf "STOP %d %s (%d %s)" i d n ty)+ (traceEventIO $ printf "START %d %s (%d %s)" i d n ty)+#else+wrapFetchInTrace _ _ _ f = f+#endif++time :: IO a -> IO (Timestamp,Microseconds,a)+time io = do+ t0 <- getTimestamp+ a <- io+ t1 <- getTimestamp+ return (t0, t1 - t0, a)++-- | Start all the async fetches first, then perform the sync fetches before+-- getting the results of the async fetches.+scheduleFetches :: [FetchToDo] -> IO [IO ()]+scheduleFetches fetches = do+ fully_async_fetches+ waits <- future_fetches+ async_fetches sync_fetches+ return waits+ where+ fully_async_fetches :: IO ()+ fully_async_fetches =+ sequence_ [f reqs | FetchToDo reqs (BackgroundFetch f) <- fetches]++ future_fetches :: IO [IO ()]+ future_fetches = sequence [f reqs | FetchToDo reqs (FutureFetch f) <- fetches]++ async_fetches :: IO () -> IO ()+ async_fetches = compose [f reqs | FetchToDo reqs (AsyncFetch f) <- fetches]++ sync_fetches :: IO ()+ sync_fetches = sequence_ [f reqs | FetchToDo reqs (SyncFetch f) <- fetches]
+ Haxl/Core/Flags.hs view
@@ -0,0 +1,75 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE CPP #-}++-- |+-- The 'Flags' type and related functions. This module is provided+-- for access to Haxl internals only; most users should import+-- "Haxl.Core" instead.+--+module Haxl.Core.Flags+ (+ -- * Tracing flags+ Flags(..)+ , defaultFlags+ , ifTrace+ , ifReport+ , ifProfiling+ ) where++import Control.Monad++-- ---------------------------------------------------------------------------+-- Flags++-- | Flags that control the operation of the engine.+data Flags = Flags+ { trace :: {-# UNPACK #-} !Int+ -- ^ Tracing level (0 = quiet, 3 = very verbose).+ , report :: {-# UNPACK #-} !Int+ -- ^ Report level:+ -- * 0 = quiet+ -- * 1 = quiet (legacy, this used to do something)+ -- * 2 = data fetch stats & errors+ -- * 3 = (same as 2, this used to enable errors)+ -- * 4 = profiling+ -- * 5 = log stack traces of dataFetch calls+ , caching :: {-# UNPACK #-} !Int+ -- ^ Non-zero if caching is enabled. If caching is disabled, then+ -- we still do batching and de-duplication, but do not cache+ -- results.+ , recording :: {-# UNPACK #-} !Int+ -- ^ Non-zero if recording is enabled. This allows tests to record cache+ -- calls for datasources by making uncachedRequest behave like dataFetch+ }++defaultFlags :: Flags+defaultFlags = Flags+ { trace = 0+ , report = 0+ , caching = 1+ , recording = 0+ }++#if __GLASGOW_HASKELL__ >= 710+#define FUNMONAD Monad m+#else+#define FUNMONAD (Functor m, Monad m)+#endif++-- | Runs an action if the tracing level is above the given threshold.+ifTrace :: FUNMONAD => Flags -> Int -> m a -> m ()+ifTrace flags i = when (trace flags >= i) . void++-- | Runs an action if the report level is above the given threshold.+ifReport :: FUNMONAD => Flags -> Int -> m a -> m ()+ifReport flags i = when (report flags >= i) . void++ifProfiling :: FUNMONAD => Flags -> m a -> m ()+ifProfiling flags = when (report flags >= 4) . void++#undef FUNMONAD
Haxl/Core/Memo.hs view
@@ -2,28 +2,48 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-} --- | Most users should import "Haxl.Core" instead of importing this--- module directly.-module Haxl.Core.Memo (- memo,- memoFingerprint,- MemoFingerprintKey(..),- memoize, memoize1, memoize2-) where+-- |+-- Memoization support. This module is provided for access to Haxl+-- internals only; most users should import "Haxl.Core" instead.+--+module Haxl.Core.Memo+ (+ -- * Basic memoization+ cachedComputation+ , preCacheComputation + -- * High-level memoization+ , memo+ , memoFingerprint+ , MemoFingerprintKey(..)+ , memoize, memoize1, memoize2++ -- * Local memoization+ , newMemo+ , newMemoWith+ , prepareMemo+ , runMemo+ ) where+ #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif+import Control.Exception as Exception hiding (throw)+import Data.IORef+import qualified Data.HashMap.Strict as HashMap import Data.Text (Text) import Data.Typeable import Data.Hashable@@ -31,9 +51,245 @@ import GHC.Prim (Addr#) +import Haxl.Core.Exception+import Haxl.Core.DataCache as DataCache+import Haxl.Core.Flags import Haxl.Core.Monad+import Haxl.Core.Profile -- -----------------------------------------------------------------------------+-- Memoization++-- | 'cachedComputation' memoizes a Haxl computation. The key is a+-- request.+--+-- /Note:/ These cached computations will /not/ be included in the output+-- of 'dumpCacheAsHaskell'.+--+cachedComputation+ :: forall req u a.+ ( Eq (req a)+ , Hashable (req a)+ , Typeable (req a))+ => req a -> GenHaxl u a -> GenHaxl u a++cachedComputation req haxl = GenHaxl $ \env@Env{..} -> do+ cache <- readIORef memoRef+ ifProfiling flags $+ modifyIORef' profRef (incrementMemoHitCounterFor profLabel)+ case DataCache.lookup req cache of+ Just ivar -> unHaxl (getIVar ivar) env+ Nothing -> do+ ivar <- newIVar+ writeIORef memoRef $! DataCache.insertNotShowable req ivar cache+ unHaxl (execMemoNow haxl ivar) env+++-- | Like 'cachedComputation', but fails if the cache is already+-- populated.+--+-- Memoization can be (ab)used to "mock" a cached computation, by+-- pre-populating the cache with an alternative implementation. In+-- that case we don't want the operation to populate the cache to+-- silently succeed if the cache is already populated.+--+preCacheComputation+ :: forall req u a.+ ( Eq (req a)+ , Hashable (req a)+ , Typeable (req a))+ => req a -> GenHaxl u a -> GenHaxl u a+preCacheComputation req haxl = GenHaxl $ \env@Env{..} -> do+ cache <- readIORef memoRef+ ifProfiling flags $+ modifyIORef' profRef (incrementMemoHitCounterFor profLabel)+ case DataCache.lookup req cache of+ Just _ -> return $ Throw $ toException $ InvalidParameter+ "preCacheComputation: key is already cached"+ Nothing -> do+ ivar <- newIVar+ writeIORef memoRef $! DataCache.insertNotShowable req ivar cache+ unHaxl (execMemoNow haxl ivar) env++-- -----------------------------------------------------------------------------+-- Memoization++newtype MemoVar u a = MemoVar (IORef (MemoStatus u a))++data MemoStatus u a+ = MemoEmpty+ | MemoReady (GenHaxl u a)+ | MemoRun {-# UNPACK #-} !(IVar u a)++-- | Create a new @MemoVar@ for storing a memoized computation. The created+-- @MemoVar@ is initially empty, not tied to any specific computation. Running+-- this memo (with @runMemo@) without preparing it first (with @prepareMemo@)+-- will result in an exception.+newMemo :: GenHaxl u (MemoVar u a)+newMemo = unsafeLiftIO $ MemoVar <$> newIORef MemoEmpty++-- | Store a computation within a supplied @MemoVar@. Any memo stored within the+-- @MemoVar@ already (regardless of completion) will be discarded, in favor of+-- the supplied computation. A @MemoVar@ must be prepared before it is run.+prepareMemo :: MemoVar u a -> GenHaxl u a -> GenHaxl u ()+prepareMemo (MemoVar memoRef) memoCmp+ = unsafeLiftIO $ writeIORef memoRef (MemoReady memoCmp)++-- | Convenience function, combines @newMemo@ and @prepareMemo@.+newMemoWith :: GenHaxl u a -> GenHaxl u (MemoVar u a)+newMemoWith memoCmp = do+ memoVar <- newMemo+ prepareMemo memoVar memoCmp+ return memoVar++-- | Continue the memoized computation within a given @MemoVar@.+-- Notes:+--+-- 1. If the memo contains a complete result, return that result.+-- 2. If the memo contains an in-progress computation, continue it as far as+-- possible for this round.+-- 3. If the memo is empty (it was not prepared), throw an error.+--+-- For example, to memoize the computation @one@ given by:+--+-- > one :: Haxl Int+-- > one = return 1+--+-- use:+--+-- > do+-- > oneMemo <- newMemoWith one+-- > let memoizedOne = runMemo aMemo one+-- > oneResult <- memoizedOne+--+-- To memoize mutually dependent computations such as in:+--+-- > h :: Haxl Int+-- > h = do+-- > a <- f+-- > b <- g+-- > return (a + b)+-- > where+-- > f = return 42+-- > g = succ <$> f+--+-- without needing to reorder them, use:+--+-- > h :: Haxl Int+-- > h = do+-- > fMemoRef <- newMemo+-- > gMemoRef <- newMemo+-- >+-- > let f = runMemo fMemoRef+-- > g = runMemo gMemoRef+-- >+-- > prepareMemo fMemoRef $ return 42+-- > prepareMemo gMemoRef $ succ <$> f+-- >+-- > a <- f+-- > b <- g+-- > return (a + b)+--+runMemo :: MemoVar u a -> GenHaxl u a+runMemo (MemoVar memoRef) = GenHaxl $ \env -> do+ stored <- readIORef memoRef+ case stored of+ -- Memo was not prepared first; throw an exception.+ MemoEmpty -> raise $ CriticalError "Attempting to run empty memo."+ -- Memo has been prepared but not run yet+ MemoReady cont -> do+ ivar <- newIVar+ writeIORef memoRef (MemoRun ivar)+ unHaxl (execMemoNow cont ivar) env+ -- The memo has already been run, get (or wait for) for the result+ MemoRun ivar -> unHaxl (getIVar ivar) env+++execMemoNow :: GenHaxl u a -> IVar u a -> GenHaxl u a+execMemoNow cont ivar = GenHaxl $ \env -> do+ let !ienv = imperative env -- don't speculate under memoized things+ r <- Exception.try $ unHaxl cont ienv+ case r of+ Left e -> do+ rethrowAsyncExceptions e+ putIVar ivar (ThrowIO e) env+ throwIO e+ Right (Done a) -> do+ putIVar ivar (Ok a) env+ return (Done a)+ Right (Throw ex) -> do+ putIVar ivar (ThrowHaxl ex) env+ return (Throw ex)+ Right (Blocked ivar' cont) -> do+ addJob env (toHaxl cont) ivar ivar'+ return (Blocked ivar (Cont (getIVar ivar)))++-- -----------------------------------------------------------------------------+-- 1-ary and 2-ary memo functions++newtype MemoVar1 u a b = MemoVar1 (IORef (MemoStatus1 u a b))+newtype MemoVar2 u a b c = MemoVar2 (IORef (MemoStatus2 u a b c))++data MemoStatus1 u a b+ = MemoEmpty1+ | MemoTbl1 (a -> GenHaxl u b) (HashMap.HashMap a (MemoVar u b))++data MemoStatus2 u a b c+ = MemoEmpty2+ | MemoTbl2+ (a -> b -> GenHaxl u c)+ (HashMap.HashMap a (HashMap.HashMap b (MemoVar u c)))++newMemo1 :: GenHaxl u (MemoVar1 u a b)+newMemo1 = unsafeLiftIO $ MemoVar1 <$> newIORef MemoEmpty1++newMemoWith1 :: (a -> GenHaxl u b) -> GenHaxl u (MemoVar1 u a b)+newMemoWith1 f = newMemo1 >>= \r -> prepareMemo1 r f >> return r++prepareMemo1 :: MemoVar1 u a b -> (a -> GenHaxl u b) -> GenHaxl u ()+prepareMemo1 (MemoVar1 r) f+ = unsafeLiftIO $ writeIORef r (MemoTbl1 f HashMap.empty)++runMemo1 :: (Eq a, Hashable a) => MemoVar1 u a b -> a -> GenHaxl u b+runMemo1 (MemoVar1 r) k = unsafeLiftIO (readIORef r) >>= \case+ MemoEmpty1 -> throw $ CriticalError "Attempting to run empty memo."+ MemoTbl1 f h -> case HashMap.lookup k h of+ Nothing -> do+ x <- newMemoWith (f k)+ unsafeLiftIO $ writeIORef r (MemoTbl1 f (HashMap.insert k x h))+ runMemo x+ Just v -> runMemo v++newMemo2 :: GenHaxl u (MemoVar2 u a b c)+newMemo2 = unsafeLiftIO $ MemoVar2 <$> newIORef MemoEmpty2++newMemoWith2 :: (a -> b -> GenHaxl u c) -> GenHaxl u (MemoVar2 u a b c)+newMemoWith2 f = newMemo2 >>= \r -> prepareMemo2 r f >> return r++prepareMemo2 :: MemoVar2 u a b c -> (a -> b -> GenHaxl u c) -> GenHaxl u ()+prepareMemo2 (MemoVar2 r) f+ = unsafeLiftIO $ writeIORef r (MemoTbl2 f HashMap.empty)++runMemo2 :: (Eq a, Hashable a, Eq b, Hashable b)+ => MemoVar2 u a b c+ -> a -> b -> GenHaxl u c+runMemo2 (MemoVar2 r) k1 k2 = unsafeLiftIO (readIORef r) >>= \case+ MemoEmpty2 -> throw $ CriticalError "Attempting to run empty memo."+ MemoTbl2 f h1 -> case HashMap.lookup k1 h1 of+ Nothing -> do+ v <- newMemoWith (f k1 k2)+ unsafeLiftIO $ writeIORef r+ (MemoTbl2 f (HashMap.insert k1 (HashMap.singleton k2 v) h1))+ runMemo v+ Just h2 -> case HashMap.lookup k2 h2 of+ Nothing -> do+ v <- newMemoWith (f k1 k2)+ unsafeLiftIO $ writeIORef r+ (MemoTbl2 f (HashMap.insert k1 (HashMap.insert k2 v h2) h1))+ runMemo v+ Just v -> runMemo v++-- ----------------------------------------------------------------------------- -- A key type that can be used for memoizing computations by a Text key -- | Memoize a computation using an arbitrary key. The result will be@@ -115,8 +371,8 @@ -- in a @MemoVar@ (which @memoize@ creates), and returns the stored result on -- subsequent invocations. This permits the creation of local memos, whose -- lifetimes are scoped to the current function, rather than the entire request.-memoize :: GenHaxl u a -> GenHaxl u a-memoize a = newMemoWith a >>= runMemo+memoize :: GenHaxl u a -> GenHaxl u (GenHaxl u a)+memoize a = runMemo <$> newMemoWith a -- | Transform a 1-argument function returning a Haxl computation into a -- memoized version of itself.@@ -127,10 +383,10 @@ -- -- e.g.: ----- allFriends :: [Int] -> GenHaxl u [Int]--- allFriends ids = do--- memoizedFriendsOf <- memoize1 friendsOf--- concat <$> mapM memoizeFriendsOf ids+-- > allFriends :: [Int] -> GenHaxl u [Int]+-- > allFriends ids = do+-- > memoizedFriendsOf <- memoize1 friendsOf+-- > concat <$> mapM memoizeFriendsOf ids -- -- The above implementation will not invoke the underlying @friendsOf@ -- repeatedly for duplicate values in @ids@.
Haxl/Core/Monad.hs view
@@ -2,1260 +2,764 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.--{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MagicHash #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternGuards #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-#if __GLASGOW_HASKELL >= 800-{-# OPTIONS_GHC -Wno-name-shadowing #-}-#else-{-# OPTIONS_GHC -fno-warn-name-shadowing #-}-#endif---- | The implementation of the 'Haxl' monad. Most users should--- import "Haxl.Core" instead of importing this module directly.-module Haxl.Core.Monad (- -- * The monad- GenHaxl (..), runHaxl,- env, withEnv, withLabel, withFingerprintLabel,-- -- * Env- Env(..), Caches, caches, initEnvWithData, initEnv, emptyEnv,-- -- * Exceptions- throw, catch, catchIf, try, tryToHaxlException,-- -- * Data fetching and caching- ShowReq, dataFetch, dataFetchWithShow, uncachedRequest, cacheRequest,- cacheResult, cacheResultWithShow, cachedComputation,- dumpCacheAsHaskell, dumpCacheAsHaskellFn,-- -- * Memoization Machinery- newMemo, newMemoWith, prepareMemo, runMemo,-- newMemo1, newMemoWith1, prepareMemo1, runMemo1,- newMemo2, newMemoWith2, prepareMemo2, runMemo2,-- -- * Unsafe operations- unsafeLiftIO, unsafeToHaxlException,-- -- * Parallel operaitons- pAnd, pOr- ) where--import Haxl.Core.Types-import Haxl.Core.ShowP-import Haxl.Core.StateStore-import Haxl.Core.Exception-import Haxl.Core.RequestStore-import Haxl.Core.Util-import Haxl.Core.DataCache as DataCache--import qualified Data.Text as Text-import qualified Control.Monad.Catch as Catch-import Control.Exception (Exception(..), SomeException)-#if __GLASGOW_HASKELL__ >= 710-import GHC.Conc (getAllocationCounter, setAllocationCounter)-#endif-import Control.Monad-import qualified Control.Exception as Exception-#if __GLASGOW_HASKELL__ < 710-import Control.Applicative hiding (Const)-#endif-import Control.DeepSeq-import GHC.Exts (IsString(..), Addr#)-#if __GLASGOW_HASKELL__ < 706-import Prelude hiding (catch)-#endif-import Data.Functor.Constant-import Data.Hashable-import qualified Data.HashMap.Strict as HashMap-import qualified Data.HashSet as HashSet-import Data.IORef-import Data.List-import qualified Data.Map as Map-import Data.Monoid-import Data.Time-import Data.Typeable-import Text.Printf-import Text.PrettyPrint hiding ((<>))-import Control.Arrow (left)--#ifdef EVENTLOG-import Control.Exception (bracket_)-import Debug.Trace (traceEventIO)-#endif--#ifdef PROFILING-import GHC.Stack-#endif--#if __GLASGOW_HASKELL__ < 710-import Data.Int (Int64)--getAllocationCounter :: IO Int64-getAllocationCounter = return 0--setAllocationCounter :: Int64 -> IO ()-setAllocationCounter _ = return ()-#endif---- -------------------------------------------------------------------------------- The environment---- | The data we carry around in the Haxl monad.-data Env u = Env- { cacheRef :: {-# UNPACK #-} !(IORef (DataCache ResultVar))- -- cached data fetches- , memoRef :: {-# UNPACK #-} !(IORef (DataCache (MemoVar u)))- -- memoized computations- , flags :: !Flags- -- conservatively not unpacking, because this is passed- -- to 'fetch' and would need to be rebuilt.- , userEnv :: u- , statsRef :: {-# UNPACK #-} !(IORef Stats)- , profLabel :: ProfileLabel- , profRef :: {-# UNPACK #-} !(IORef Profile)- , states :: StateStore- -- ^ Data sources and other components can store their state in- -- here. Items in this store must be instances of 'StateKey'.- }--type Caches u = (IORef (DataCache ResultVar), IORef (DataCache (MemoVar u)))--caches :: Env u -> Caches u-caches env = (cacheRef env, memoRef env)---- | Initialize an environment with a 'StateStore', an input map, a--- preexisting 'DataCache', and a seed for the random number generator.-initEnvWithData :: StateStore -> u -> Caches u -> IO (Env u)-initEnvWithData states e (cref, mref) = do- sref <- newIORef emptyStats- pref <- newIORef emptyProfile- return Env- { cacheRef = cref- , memoRef = mref- , flags = defaultFlags- , userEnv = e- , states = states- , statsRef = sref- , profLabel = "MAIN"- , profRef = pref- }---- | Initializes an environment with 'StateStore' and an input map.-initEnv :: StateStore -> u -> IO (Env u)-initEnv states e = do- cref <- newIORef emptyDataCache- mref <- newIORef emptyDataCache- initEnvWithData states e (cref,mref)---- | A new, empty environment.-emptyEnv :: u -> IO (Env u)-emptyEnv = initEnv stateEmpty---- -------------------------------------------------------------------------------- | The Haxl monad, which does several things:------ * It is a reader monad for 'Env' and 'IORef' 'RequestStore', The--- latter is the current batch of unsubmitted data fetch requests.------ * It is a concurrency, or resumption, monad. A computation may run--- partially and return 'Blocked', in which case the framework should--- perform the outstanding requests in the 'RequestStore', and then--- resume the computation.------ * The Applicative combinator '<*>' explores /both/ branches in the--- event that the left branch is 'Blocked', so that we can collect--- multiple requests and submit them as a batch.------ * It contains IO, so that we can perform real data fetching.----newtype GenHaxl u a = GenHaxl- { unHaxl :: Env u -> IORef (RequestStore u) -> IO (Result u a) }---- | The result of a computation is either 'Done' with a value, 'Throw'--- with an exception, or 'Blocked' on the result of a data fetch with--- a continuation.-data Result u a- = Done a- | Throw SomeException- | Blocked (Cont u a)--data Cont u a- = Cont (GenHaxl u a)- | forall b. Cont u b :>>= (b -> GenHaxl u a)- | forall b. (Cont u (b -> a)) :<*> (Cont u b)- | forall b. (b -> a) :<$> (Cont u b)--toHaxl :: Cont u a -> GenHaxl u a-toHaxl (Cont haxl) = haxl-toHaxl ((m :>>= k1) :>>= k2) = toHaxl (m :>>= (k1 >=> k2)) -- for seql-toHaxl (c :>>= k) = toHaxl c >>= k-toHaxl ((f :<$> i) :<*> (g :<$> j)) =- toHaxl (((\x y -> f x (g y)) :<$> i) :<*> j) -- See Note [Tree]-toHaxl (f :<*> x) = toHaxl f <*> toHaxl x-toHaxl (f :<$> (g :<$> x)) = toHaxl ((f . g) :<$> x) -- fmap fusion-toHaxl (f :<$> x) = fmap f (toHaxl x)---- Note [Tree]--- This implements the following re-association:------ <*>--- / \--- <$> <$>--- / \ / \--- f i g j------ to:------ <*>--- / \--- <$> j--- / \ where h = (\x y -> f x (g y))--- h i------ I suspect this is mostly useful because it eliminates one :<$> constructor--- within the Blocked returned by `tree 1`, which is replicated a lot by the--- tree benchmark (tree 1 is near the leaves). So this rule might just be--- optimizing for a microbenchmark.--instance (Show a) => Show (Result u a) where- show (Done a) = printf "Done(%s)" $ show a- show (Throw e) = printf "Throw(%s)" $ show e- show Blocked{} = "Blocked"--instance Monad (GenHaxl u) where- return a = GenHaxl $ \_env _ref -> return (Done a)- GenHaxl m >>= k = GenHaxl $ \env ref -> do- e <- m env ref- case e of- Done a -> unHaxl (k a) env ref- Throw e -> return (Throw e)- Blocked cont -> return (Blocked (cont :>>= k))- fail msg = GenHaxl $ \_env _ref ->- return $ Throw $ toException $ MonadFail $ Text.pack msg-- -- We really want the Applicative version of >>- (>>) = (*>)--instance Functor (GenHaxl u) where- fmap f (GenHaxl m) = GenHaxl $ \env ref -> do- r <- m env ref- case r of- Done a -> return (Done (f a))- Throw e -> return (Throw e)- Blocked a' -> return (Blocked (f :<$> a'))--instance Applicative (GenHaxl u) where- pure = return- GenHaxl f <*> GenHaxl a = GenHaxl $ \env ref -> do- r <- f env ref- case r of- Throw e -> return (Throw e)- Done f' -> do- ra <- a env ref- case ra of- Done a' -> return (Done (f' a'))- Throw e -> return (Throw e)- Blocked a' -> return (Blocked (f' :<$> a'))- Blocked f' -> do- ra <- a env ref -- left is blocked, explore the right- case ra of- Done a' -> return (Blocked (($ a') :<$> f'))- Throw e -> return (Blocked (f' :<*> Cont (throw e)))- Blocked a' -> return (Blocked (f' :<*> a'))---- | Runs a 'Haxl' computation in an 'Env'.-runHaxl :: Env u -> GenHaxl u a -> IO a-#ifdef EVENTLOG-runHaxl env h = do- let go !n env c = do- traceEventIO "START computation"- ref <- newIORef noRequests- e <- (unHaxl $ toHaxl c) env ref- traceEventIO "STOP computation"- case e of- Done a -> return a- Throw e -> Exception.throw e- Blocked cont -> do- bs <- readIORef ref- writeIORef ref noRequests -- Note [RoundId]- traceEventIO "START performFetches"- n' <- performFetches n env bs- traceEventIO "STOP performFetches"- when (caching (flags env) == 0) $- writeIORef (cacheRef env) emptyDataCache- go n' env cont- traceEventIO "START runHaxl"- r <- go 0 env (Cont h)- traceEventIO "STOP runHaxl"- return r-#else-runHaxl env (GenHaxl haxl) = do- ref <- newIORef noRequests- e <- haxl env ref- case e of- Done a -> return a- Throw e -> Exception.throw e- Blocked cont -> do- bs <- readIORef ref- writeIORef ref noRequests -- Note [RoundId]- void (performFetches 0 env bs)- when (caching (flags env) == 0) $- writeIORef (cacheRef env) emptyDataCache- runHaxl env (toHaxl cont)-#endif---- | Extracts data from the 'Env'.-env :: (Env u -> a) -> GenHaxl u a-env f = GenHaxl $ \env _ref -> return (Done (f env))---- | Returns a version of the Haxl computation which always uses the--- provided 'Env', ignoring the one specified by 'runHaxl'.-withEnv :: Env u -> GenHaxl u a -> GenHaxl u a-withEnv newEnv (GenHaxl m) = GenHaxl $ \_env ref -> do- r <- m newEnv ref- case r of- Done a -> return (Done a)- Throw e -> return (Throw e)- Blocked k -> return (Blocked (Cont (withEnv newEnv (toHaxl k))))---- | Label a computation so profiling data is attributed to the label.-withLabel :: ProfileLabel -> GenHaxl u a -> GenHaxl u a-withLabel l (GenHaxl m) = GenHaxl $ \env ref ->- if report (flags env) < 4- then m env ref- else collectProfileData l m env ref---- | Label a computation so profiling data is attributed to the label.--- Intended only for internal use by 'memoFingerprint'.-withFingerprintLabel :: Addr# -> Addr# -> GenHaxl u a -> GenHaxl u a-withFingerprintLabel mnPtr nPtr (GenHaxl m) = GenHaxl $ \env ref ->- if report (flags env) < 4- then m env ref- else collectProfileData- (Text.unpackCString# mnPtr <> "." <> Text.unpackCString# nPtr)- m env ref---- | Collect profiling data and attribute it to given label.-collectProfileData- :: ProfileLabel- -> (Env u -> IORef (RequestStore u) -> IO (Result u a))- -> Env u -> IORef (RequestStore u)- -> IO (Result u a)-collectProfileData l m env ref = do- a0 <- getAllocationCounter- r <- m env{profLabel=l} ref -- what if it throws?- a1 <- getAllocationCounter- modifyProfileData env l (a0 - a1)- -- So we do not count the allocation overhead of modifyProfileData- setAllocationCounter a1- case r of- Done a -> return (Done a)- Throw e -> return (Throw e)- Blocked k -> return (Blocked (Cont (withLabel l (toHaxl k))))-{-# INLINE collectProfileData #-}--modifyProfileData :: Env u -> ProfileLabel -> AllocCount -> IO ()-modifyProfileData env label allocs =- modifyIORef' (profRef env) $ \ p ->- p { profile =- HashMap.insertWith updEntry label newEntry .- HashMap.insertWith updCaller caller newCaller $- profile p }- where caller = profLabel env- newEntry =- emptyProfileData- { profileAllocs = allocs- , profileDeps = HashSet.singleton caller }- updEntry _ old =- old { profileAllocs = profileAllocs old + allocs- , profileDeps = HashSet.insert caller (profileDeps old) }- -- subtract allocs from caller, so they are not double counted- -- we don't know the caller's caller, but it will get set on- -- the way back out, so an empty hashset is fine for now- newCaller =- emptyProfileData { profileAllocs = -allocs }- updCaller _ old =- old { profileAllocs = profileAllocs old - allocs }--incrementMemoHitCounterFor :: ProfileLabel -> Profile -> Profile-incrementMemoHitCounterFor lbl p =- p { profile = HashMap.adjust incrementMemoHitCounter lbl (profile p) }--incrementMemoHitCounter :: ProfileData -> ProfileData-incrementMemoHitCounter pd = pd { profileMemoHits = succ (profileMemoHits pd) }---- -------------------------------------------------------------------------------- Exceptions---- | Throw an exception in the Haxl monad-throw :: (Exception e) => e -> GenHaxl u a-throw e = GenHaxl $ \_env _ref -> raise e--raise :: (Exception e) => e -> IO (Result u a)-raise e-#ifdef PROFILING- | Just (HaxlException Nothing h) <- fromException somex = do- stk <- currentCallStack- return (Throw (toException (HaxlException (Just stk) h)))- | otherwise-#endif- = return (Throw somex)- where- somex = toException e---- | Catch an exception in the Haxl monad-catch :: Exception e => GenHaxl u a -> (e -> GenHaxl u a) -> GenHaxl u a-catch (GenHaxl m) h = GenHaxl $ \env ref -> do- r <- m env ref- case r of- Done a -> return (Done a)- Throw e | Just e' <- fromException e -> unHaxl (h e') env ref- | otherwise -> return (Throw e)- Blocked k -> return (Blocked (Cont (catch (toHaxl k) h)))---- | Catch exceptions that satisfy a predicate-catchIf- :: Exception e => (e -> Bool) -> GenHaxl u a -> (e -> GenHaxl u a)- -> GenHaxl u a-catchIf cond haxl handler =- catch haxl $ \e -> if cond e then handler e else throw e---- | Returns @'Left' e@ if the computation throws an exception @e@, or--- @'Right' a@ if it returns a result @a@.-try :: Exception e => GenHaxl u a -> GenHaxl u (Either e a)-try haxl = (Right <$> haxl) `catch` (return . Left)---- | @since 0.3.1.0-instance Catch.MonadThrow (GenHaxl u) where throwM = Haxl.Core.Monad.throw--- | @since 0.3.1.0-instance Catch.MonadCatch (GenHaxl u) where catch = Haxl.Core.Monad.catch---- -------------------------------------------------------------------------------- Unsafe operations---- | Under ordinary circumstances this is unnecessary; users of the Haxl--- monad should generally /not/ perform arbitrary IO.-unsafeLiftIO :: IO a -> GenHaxl u a-unsafeLiftIO m = GenHaxl $ \_env _ref -> Done <$> m---- | Convert exceptions in the underlying IO monad to exceptions in--- the Haxl monad. This is morally unsafe, because you could then--- catch those exceptions in Haxl and observe the underlying execution--- order. Not to be exposed to user code.-unsafeToHaxlException :: GenHaxl u a -> GenHaxl u a-unsafeToHaxlException (GenHaxl m) = GenHaxl $ \env ref -> do- r <- m env ref `Exception.catch` \e -> return (Throw e)- case r of- Blocked c -> return (Blocked (Cont (unsafeToHaxlException (toHaxl c))))- other -> return other---- | Like 'try', but lifts all exceptions into the 'HaxlException'--- hierarchy. Uses 'unsafeToHaxlException' internally. Typically--- this is used at the top level of a Haxl computation, to ensure that--- all exceptions are caught.-tryToHaxlException :: GenHaxl u a -> GenHaxl u (Either HaxlException a)-tryToHaxlException h = left asHaxlException <$> try (unsafeToHaxlException h)----- -------------------------------------------------------------------------------- Data fetching and caching---- | Possible responses when checking the cache.-data CacheResult a- -- | The request hadn't been seen until now.- = Uncached (ResultVar a)-- -- | The request has been seen before, but its result has not yet been- -- fetched.- | CachedNotFetched (ResultVar a)-- -- | The request has been seen before, and its result has already been- -- fetched.- | Cached (Either SomeException a)---- | Checks the data cache for the result of a request.-cached :: Request r a => Env u -> r a -> IO (CacheResult a)-cached = cachedWithInsert show DataCache.insert---- | Show functions for request and its result.-type ShowReq r a = (r a -> String, a -> String)---- Note [showFn]------ Occasionally, for tracing purposes or generating exceptions, we need to--- call 'show' on the request in a place where we *cannot* have a Show--- dictionary. (Because the function is a worker which is called by one of--- the *WithShow variants that take explicit show functions via a ShowReq--- argument.) None of the functions that does this is exported, so this is--- hidden from the Haxl user.---- | Checks the data cache for the result of a request, inserting new results--- with the given function.-cachedWithInsert- :: Typeable (r a)- => (r a -> String) -- See Note [showFn]- -> (r a -> ResultVar a -> DataCache ResultVar -> DataCache ResultVar) -> Env u- -> r a -> IO (CacheResult a)-cachedWithInsert showFn insertFn env req = do- let- doFetch insertFn request cache = do- rvar <- newEmptyResult- writeIORef (cacheRef env) $! insertFn request rvar cache- return (Uncached rvar)- cache <- readIORef (cacheRef env)- case DataCache.lookup req cache of- Nothing -> doFetch insertFn req cache- Just rvar -> do- mb <- tryReadResult rvar- case mb of- Nothing -> return (CachedNotFetched rvar)- -- Use the cached result, even if it was an error.- Just r -> do- ifTrace (flags env) 3 $ putStrLn $ case r of- Left _ -> "Cached error: " ++ showFn req- Right _ -> "Cached request: " ++ showFn req- return (Cached r)---- | Record the call stack for a data fetch in the Stats. Only useful--- when profiling.-logFetch :: Env u -> (r a -> String) -> r a -> IO ()-#ifdef PROFILING-logFetch env showFn req = do- ifReport (flags env) 5 $ do- stack <- currentCallStack- modifyIORef' (statsRef env) $ \(Stats s) ->- Stats (FetchCall (showFn req) stack : s)-#else-logFetch _ _ _ = return ()-#endif---- | Performs actual fetching of data for a 'Request' from a 'DataSource'.-dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a-dataFetch = dataFetchWithInsert show DataCache.insert---- | Performs actual fetching of data for a 'Request' from a 'DataSource', using--- the given show functions for requests and their results.-dataFetchWithShow- :: (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a))- => ShowReq r a- -> r a -> GenHaxl u a-dataFetchWithShow (showReq, showRes) = dataFetchWithInsert showReq- (DataCache.insertWithShow showReq showRes)---- | Performs actual fetching of data for a 'Request' from a 'DataSource', using--- the given function to insert requests in the cache.-dataFetchWithInsert- :: (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a))- => (r a -> String) -- See Note [showFn]- -> (r a -> ResultVar a -> DataCache ResultVar -> DataCache ResultVar)- -> r a- -> GenHaxl u a-dataFetchWithInsert showFn insertFn req = GenHaxl $ \env ref -> do- -- First, check the cache- res <- cachedWithInsert showFn insertFn env req- ifProfiling (flags env) $ addProfileFetch env req- case res of- -- Not seen before: add the request to the RequestStore, so it- -- will be fetched in the next round.- Uncached rvar -> do- logFetch env showFn req- modifyIORef' ref $ \bs -> addRequest (BlockedFetch req rvar) bs- return $ Blocked (Cont (continueFetch showFn req rvar))-- -- Seen before but not fetched yet. We're blocked, but we don't have- -- to add the request to the RequestStore.- CachedNotFetched rvar ->- return (Blocked (Cont (continueFetch showFn req rvar)))-- -- Cached: either a result, or an exception- Cached (Left ex) -> return (Throw ex)- Cached (Right a) -> return (Done a)--{-# NOINLINE addProfileFetch #-}-addProfileFetch- :: (DataSourceName r, Eq (r a), Hashable (r a), Typeable (r a))- => Env u -> r a -> IO ()-addProfileFetch env req = do- c <- getAllocationCounter- modifyIORef' (profRef env) $ \ p ->- let- dsName :: Text.Text- dsName = dataSourceName req-- upd :: Round -> ProfileData -> ProfileData- upd round d =- d { profileFetches = Map.alter (Just . f) round (profileFetches d) }-- f Nothing = HashMap.singleton dsName 1- f (Just hm) = HashMap.insertWith (+) dsName 1 hm- in case DataCache.lookup req (profileCache p) of- Nothing ->- let r = profileRound p- in p { profile = HashMap.adjust (upd r) (profLabel env) (profile p)- , profileCache =- DataCache.insertNotShowable req (Constant r) (profileCache p)- }- Just (Constant r) ->- p { profile = HashMap.adjust (upd r) (profLabel env) (profile p) }- -- So we do not count the allocation overhead of addProfileFetch- setAllocationCounter c---- | A data request that is not cached. This is not what you want for--- normal read requests, because then multiple identical requests may--- return different results, and this invalidates some of the--- properties that we expect Haxl computations to respect: that data--- fetches can be aribtrarily reordered, and identical requests can be--- commoned up, for example.------ 'uncachedRequest' is useful for performing writes, provided those--- are done in a safe way - that is, not mixed with reads that might--- conflict in the same Haxl computation.----uncachedRequest :: (DataSource u r, Show (r a)) => r a -> GenHaxl u a-uncachedRequest req = GenHaxl $ \_env ref -> do- rvar <- newEmptyResult- modifyIORef' ref $ \bs -> addRequest (BlockedFetch req rvar) bs- return $ Blocked (Cont (continueFetch show req rvar))--continueFetch- :: (r a -> String) -- See Note [showFn]- -> r a -> ResultVar a -> GenHaxl u a-continueFetch showFn req rvar = GenHaxl $ \_env _ref -> do- m <- tryReadResult rvar- case m of- Nothing -> raise . DataSourceError $- Text.pack (showFn req) <> " did not set contents of result var"- Just r -> done r---- | Transparently provides caching. Useful for datasources that can--- return immediately, but also caches values. Exceptions thrown by--- the IO operation (except for asynchronous exceptions) are--- propagated into the Haxl monad and can be caught by 'catch' and--- 'try'.-cacheResult :: Request r a => r a -> IO a -> GenHaxl u a-cacheResult = cacheResultWithInsert show DataCache.insert---- | Transparently provides caching in the same way as 'cacheResult', but uses--- the given functions to show requests and their results.-cacheResultWithShow- :: (Eq (r a), Hashable (r a), Typeable (r a))- => ShowReq r a -> r a -> IO a -> GenHaxl u a-cacheResultWithShow (showReq, showRes) = cacheResultWithInsert showReq- (DataCache.insertWithShow showReq showRes)---- Transparently provides caching, using the given function to insert requests--- into the cache.-cacheResultWithInsert- :: Typeable (r a)- => (r a -> String) -- See Note [showFn]- -> (r a -> ResultVar a -> DataCache ResultVar -> DataCache ResultVar) -> r a- -> IO a -> GenHaxl u a-cacheResultWithInsert showFn insertFn req val = GenHaxl $ \env _ref -> do- cachedResult <- cachedWithInsert showFn insertFn env req- case cachedResult of- Uncached rvar -> do- result <- Exception.try val- putResult rvar result- case result of- Left e -> do rethrowAsyncExceptions e; done result- _other -> done result- Cached result -> done result- CachedNotFetched _ -> corruptCache- where- corruptCache = raise . DataSourceError $ Text.concat- [ Text.pack (showFn req)- , " has a corrupted cache value: these requests are meant to"- , " return immediately without an intermediate value. Either"- , " the cache was updated incorrectly, or you're calling"- , " cacheResult on a query that involves a blocking fetch."- ]---- | Inserts a request/result pair into the cache. Throws an exception--- if the request has already been issued, either via 'dataFetch' or--- 'cacheRequest'.------ This can be used to pre-populate the cache when running tests, to--- avoid going to the actual data source and ensure that results are--- deterministic.----cacheRequest- :: Request req a => req a -> Either SomeException a -> GenHaxl u ()-cacheRequest request result = GenHaxl $ \env _ref -> do- res <- cached env request- case res of- Uncached rvar -> do- -- request was not in the cache: insert the result and continue- putResult rvar result- return $ Done ()-- -- It is an error if the request is already in the cache. We can't test- -- whether the cached result is the same without adding an Eq constraint,- -- and we don't necessarily have Eq for all results.- _other -> raise $- DataSourceError "cacheRequest: request is already in the cache"--instance IsString a => IsString (GenHaxl u a) where- fromString s = return (fromString s)---- | Issues a batch of fetches in a 'RequestStore'. After--- 'performFetches', all the requests in the 'RequestStore' are--- complete, and all of the 'ResultVar's are full.-performFetches :: forall u. Int -> Env u -> RequestStore u -> IO Int-performFetches n env reqs = do- let f = flags env- sref = statsRef env- jobs = contents reqs- !n' = n + length jobs-- t0 <- getCurrentTime- a0 <- getAllocationCounter-- let- roundstats =- [ (dataSourceName (getReq reqs), length reqs)- | BlockedFetches reqs <- jobs ]- where- getReq :: [BlockedFetch r] -> r a- getReq = undefined-- ifTrace f 1 $- printf "Batch data fetch (%s)\n" $- intercalate (", "::String) $- map (\(name,num) -> printf "%d %s" num (Text.unpack name)) roundstats-- ifTrace f 3 $- forM_ jobs $ \(BlockedFetches reqs) ->- forM_ reqs $ \(BlockedFetch r _) -> putStrLn (showp r)-- let- applyFetch (i, BlockedFetches (reqs :: [BlockedFetch r])) =- case stateGet (states env) of- Nothing ->- return (SyncFetch (mapM_ (setError e) reqs))- where e req = DataSourceError $- "data source not initialized: " <>- dataSourceName req <>- ": " <>- Text.pack (showp req)- Just state ->- return $ wrapFetchInTrace i (length reqs)- (dataSourceName (undefined :: r a))- $ wrapFetchInCatch reqs- $ fetch state f (userEnv env) reqs-- fetches <- mapM applyFetch $ zip [n..] jobs-- deepStats <-- if report f >= 2- then do- (refs, timedfetches) <- mapAndUnzipM wrapFetchInStats fetches- scheduleFetches timedfetches- mapM (fmap Just . readIORef) refs- else do- scheduleFetches fetches- return $ repeat Nothing-- failures <-- if report f >= 3- then- forM jobs $ \(BlockedFetches reqs) ->- fmap (Just . length) . flip filterM reqs $ \(BlockedFetch _ rvar) -> do- mb <- tryReadResult rvar- return $ case mb of- Just (Right _) -> False- _ -> True- else return $ repeat Nothing-- let dsroundstats = HashMap.fromList- [ (name, DataSourceRoundStats { dataSourceFetches = dsfetch- , dataSourceTime = fst <$> dsStats- , dataSourceAllocation = snd <$> dsStats- , dataSourceFailures = dsfailure- })- | ((name, dsfetch), dsStats, dsfailure) <-- zip3 roundstats deepStats failures]-- a1 <- getAllocationCounter- t1 <- getCurrentTime- let- roundtime = realToFrac (diffUTCTime t1 t0) :: Double- allocation = fromIntegral $ a0 - a1-- ifReport f 1 $- modifyIORef' sref $ \(Stats rounds) -> roundstats `deepseq`- Stats (RoundStats (microsecs roundtime) allocation dsroundstats: rounds)-- ifTrace f 1 $- printf "Batch data fetch done (%.2fs)\n" (realToFrac roundtime :: Double)-- ifProfiling f $- modifyIORef' (profRef env) $ \ p -> p { profileRound = 1 + profileRound p }-- return n'---- Catch exceptions arising from the data source and stuff them into--- the appropriate requests. We don't want any exceptions propagating--- directly from the data sources, because we want the exception to be--- thrown by dataFetch instead.----wrapFetchInCatch :: [BlockedFetch req] -> PerformFetch -> PerformFetch-wrapFetchInCatch reqs fetch =- case fetch of- SyncFetch io ->- SyncFetch (io `Exception.catch` handler)- AsyncFetch fio ->- AsyncFetch (\io -> fio io `Exception.catch` handler)- -- this might be wrong: if the outer 'fio' throws an exception,- -- then we don't know whether we have executed the inner 'io' or- -- not. If not, then we'll likely get some errors about "did- -- not set result var" later, because we haven't executed some- -- data fetches. But we can't execute 'io' in the handler,- -- because we might have already done it. It isn't possible to- -- do it completely right here, so we have to rely on data- -- sources themselves to catch (synchronous) exceptions. Async- -- exceptions aren't a problem because we're going to rethrow- -- them all the way to runHaxl anyway.- where- handler :: SomeException -> IO ()- handler e = do- rethrowAsyncExceptions e- mapM_ (forceError e) reqs-- -- Set the exception even if the request already had a result.- -- Otherwise we could be discarding an exception.- forceError e (BlockedFetch _ rvar) = do- void $ tryTakeResult rvar- putResult rvar (except e)--wrapFetchInStats :: PerformFetch -> IO (IORef (Microseconds, Int), PerformFetch)-wrapFetchInStats f = do- r <- newIORef (0, 0)- case f of- SyncFetch io -> return (r, SyncFetch (statsForIO io >>= writeIORef r))- AsyncFetch f -> do- inner_r <- newIORef (0, 0)- return (r, AsyncFetch $ \inner -> do- (totalTime, totalAlloc) <-- statsForIO (f (statsForIO inner >>= writeIORef inner_r))- (innerTime, innerAlloc) <- readIORef inner_r- writeIORef r (totalTime - innerTime, totalAlloc - innerAlloc))- where- statsForIO io = do- prevAlloc <- getAllocationCounter- t <- time io- postAlloc <- getAllocationCounter- return (t, fromIntegral $ prevAlloc - postAlloc)--wrapFetchInTrace :: Int -> Int -> Text.Text -> PerformFetch -> PerformFetch-#ifdef EVENTLOG-wrapFetchInTrace i n dsName f =- case f of- SyncFetch io -> SyncFetch (wrapF "Sync" io)- AsyncFetch fio -> AsyncFetch (wrapF "Async" . fio . unwrapF "Async")- where- d = Text.unpack dsName- wrapF :: String -> IO a -> IO a- wrapF ty = bracket_ (traceEventIO $ printf "START %d %s (%d %s)" i d n ty)- (traceEventIO $ printf "STOP %d %s (%d %s)" i d n ty)- unwrapF :: String -> IO a -> IO a- unwrapF ty = bracket_ (traceEventIO $ printf "STOP %d %s (%d %s)" i d n ty)- (traceEventIO $ printf "START %d %s (%d %s)" i d n ty)-#else-wrapFetchInTrace _ _ _ f = f-#endif--time :: IO () -> IO Microseconds-time io = do- t0 <- getCurrentTime- io- t1 <- getCurrentTime- return . microsecs . realToFrac $ t1 `diffUTCTime` t0--microsecs :: Double -> Microseconds-microsecs t = round (t * 10^(6::Int))---- | Start all the async fetches first, then perform the sync fetches before--- getting the results of the async fetches.-scheduleFetches :: [PerformFetch] -> IO()-scheduleFetches fetches = async_fetches sync_fetches- where- async_fetches :: IO () -> IO ()- async_fetches = compose [f | AsyncFetch f <- fetches]-- sync_fetches :: IO ()- sync_fetches = sequence_ [io | SyncFetch io <- fetches]----- -------------------------------------------------------------------------------- Memoization---- | Variables representing memoized computations.-newtype MemoVar u a = MemoVar (IORef (MemoStatus u a))-newtype MemoVar1 u a b = MemoVar1 (IORef (MemoStatus1 u a b))-newtype MemoVar2 u a b c = MemoVar2 (IORef (MemoStatus2 u a b c))---- | The state of a memoized computation-data MemoStatus u a- -- | Memoized computation under evaluation. The memo was last evaluated during- -- the given round, or never, if the given round is Nothing. The continuation- -- might be slightly out of date, but that's fine; the worst that can happen- -- is we do a little extra work.- = MemoInProgress (RoundId u) (GenHaxl u a)-- -- | A fully evaluated memo; here is the result.- | MemoDone (Either SomeException a)-- -- | A new memo, with a stored computation. Not empty, but has not been run- -- yet.- | MemoNew (GenHaxl u a)-- -- | An empty memo, should not be run before preparation.- | MemoEmpty---- | The state of a memoized 1-argument function.-data MemoStatus1 u a b- -- | An unprepared memo.- = MemoEmpty1- -- | A memo-table containing @MemoStatus@es for at least one in-progress memo.- | MemoTbl1 ( a -> GenHaxl u b- , HashMap.HashMap a- (MemoVar u b))--data MemoStatus2 u a b c- -- | An unprepared memo.- = MemoEmpty2- -- | A memo-table containing @MemoStatus@es for at least one in-progress memo.- | MemoTbl2 ( a -> b -> GenHaxl u c- , HashMap.HashMap a- (HashMap.HashMap b- (MemoVar u c)))--type RoundId u = IORef (RequestStore u)-{--Note [RoundId]--A token representing the round. This needs to be unique per round,-and it needs to support Eq. Fortunately the IORef RequestStore is-exactly what we need: IORef supports Eq, and we make a new one for-each round. There's a danger that storing this in the DataCache could-cause a space leak, so we stub out the contents after each round (see-runHaxl).--}---- | 'cachedComputation' memoizes a Haxl computation. The key is a--- request.------ /Note:/ These cached computations will /not/ be included in the output--- of 'dumpCacheAsHaskell'.----cachedComputation- :: forall req u a.- ( Eq (req a)- , Hashable (req a)- , Typeable (req a))- => req a -> GenHaxl u a -> GenHaxl u a-cachedComputation req haxl = do- env <- env id- cache <- unsafeLiftIO $ readIORef (memoRef env)- unsafeLiftIO $ ifProfiling (flags env) $- modifyIORef' (profRef env) (incrementMemoHitCounterFor (profLabel env))- memoVar <- case DataCache.lookup req cache of- Nothing -> do- memoVar <- newMemoWith haxl- unsafeLiftIO $ writeIORef (memoRef env) $!- DataCache.insertNotShowable req memoVar cache- return memoVar- Just memoVar -> return memoVar- runMemo memoVar---- | Lifts an 'Either' into either 'Throw' or 'Done'.-done :: Either SomeException a -> IO (Result u a)-done = return . either Throw Done---- --------------------------------------------------------------------------------- | Dump the contents of the cache as Haskell code that, when--- compiled and run, will recreate the same cache contents. For--- example, the generated code looks something like this:------ > loadCache :: GenHaxl u ()--- > loadCache = do--- > cacheRequest (ListWombats 3) (Right ([1,2,3]))--- > cacheRequest (CountAardvarks "abcabc") (Right (2))----dumpCacheAsHaskell :: GenHaxl u String-dumpCacheAsHaskell = dumpCacheAsHaskellFn "loadCache" "GenHaxl u ()"---- | Dump the contents of the cache as Haskell code that, when--- compiled and run, will recreate the same cache contents.------ Takes the name and type for the resulting function as arguments.-dumpCacheAsHaskellFn :: String -> String -> GenHaxl u String-dumpCacheAsHaskellFn fnName fnType = do- ref <- env cacheRef -- NB. cacheRef, not memoRef. We ignore memoized- -- results when dumping the cache.- entries <- unsafeLiftIO $ readIORef ref >>= showCache- let- mk_cr (req, res) =- text "cacheRequest" <+> parens (text req) <+> parens (result res)- result (Left e) = text "except" <+> parens (text (show e))- result (Right s) = text "Right" <+> parens (text s)-- return $ show $- text (fnName ++ " :: " ++ fnType) $$- text (fnName ++ " = do") $$- nest 2 (vcat (map mk_cr (concatMap snd entries))) $$- text "" -- final newline---- | Create a new @MemoVar@ for storing a memoized computation. The created--- @MemoVar@ is initially empty, not tied to any specific computation. Running--- this memo (with @runMemo@) without preparing it first (with @prepareMemo@)--- will result in an exception.-newMemo :: GenHaxl u (MemoVar u a)-newMemo = unsafeLiftIO $ MemoVar <$> newIORef MemoEmpty---- | Store a computation within a supplied @MemoVar@. Any memo stored within the--- @MemoVar@ already (regardless of completion) will be discarded, in favor of--- the supplied computation. A @MemoVar@ must be prepared before it is run.-prepareMemo :: MemoVar u a -> GenHaxl u a -> GenHaxl u ()-prepareMemo (MemoVar memoRef) memoCmp- = unsafeLiftIO $ writeIORef memoRef (MemoNew memoCmp)---- | Convenience function, combines @newMemo@ and @prepareMemo@.-newMemoWith :: GenHaxl u a -> GenHaxl u (MemoVar u a)-newMemoWith memoCmp = do- memoVar <- newMemo- prepareMemo memoVar memoCmp- return memoVar---- | Continue the memoized computation within a given @MemoVar@.--- Notes:------ 1. If the memo contains a complete result, return that result.--- 2. If the memo contains an in-progress computation, continue it as far as--- possible for this round.--- 3. If the memo is empty (it was not prepared), throw an error.------ For example, to memoize the computation @one@ given by:------ > one :: Haxl Int--- > one = return 1------ use:------ > do--- > oneMemo <- newMemoWith one--- > let memoizedOne = runMemo aMemo one--- > oneResult <- memoizedOne------ To memoize mutually dependent computations such as in:------ > h :: Haxl Int--- > h = do--- > a <- f--- > b <- g--- > return (a + b)--- > where--- > f = return 42--- > g = succ <$> f------ without needing to reorder them, use:------ > h :: Haxl Int--- > h = do--- > fMemoRef <- newMemo--- > gMemoRef <- newMemo--- >--- > let f = runMemo fMemoRef--- > g = runMemo gMemoRef--- >--- > prepareMemo fMemoRef $ return 42--- > prepareMemo gMemoRef $ succ <$> f--- >--- > a <- f--- > b <- g--- > return (a + b)----runMemo :: MemoVar u a -> GenHaxl u a-runMemo memoVar@(MemoVar memoRef) = GenHaxl $ \env rID ->- readIORef memoRef >>= \case- -- Memo was not prepared first; throw an exception.- MemoEmpty -> raise $ CriticalError "Attempting to run empty memo."- -- The memo is complete.- MemoDone result -> done result- -- Memo has just been prepared, run it.- MemoNew cont -> runContToMemo cont env rID- -- The memo is in progress, there *may* be progress to be made.- MemoInProgress rID' cont- -- The last update was performed *this* round and is still in progress;- -- nothing further can be done this round. Wait until the next round.- | rID' == rID -> return (Blocked $ Cont retryMemo)- -- This is the first time this memo is being run during this round, or- -- at all. Enough progress may have been made to continue running the- -- memo.- | otherwise -> runContToMemo cont env rID- where- -- Continuation to retry an existing memo. It is not possible to *retry* an- -- empty memo; that will throw an exception during the next round.- retryMemo = runMemo memoVar-- -- Run a continuation, and store the result in the memo reference. Any- -- exceptions thrown during the running of the memo are thrown directly; they- -- are also stored in the memoVar just in case, but we shouldn't be looking at- -- the memoVar again anyway.- --- -- If the memo is incomplete by the end of this round, update its progress- -- indicator and block.- runContToMemo cont env rID = do- result <- unHaxl cont env rID- case result of- Done a -> finalize (Right a)- Throw e -> finalize (Left e)- Blocked c -> do- writeIORef memoRef (MemoInProgress rID (toHaxl c))- return (Blocked $ Cont retryMemo)-- finalize r = writeIORef memoRef (MemoDone r) >> done r--newMemo1 :: GenHaxl u (MemoVar1 u a b)-newMemo1 = unsafeLiftIO $ MemoVar1 <$> newIORef MemoEmpty1--newMemoWith1 :: (a -> GenHaxl u b) -> GenHaxl u (MemoVar1 u a b)-newMemoWith1 f = newMemo1 >>= \r -> prepareMemo1 r f >> return r--prepareMemo1 :: MemoVar1 u a b -> (a -> GenHaxl u b) -> GenHaxl u ()-prepareMemo1 (MemoVar1 r) f- = unsafeLiftIO $ writeIORef r (MemoTbl1 (f, HashMap.empty))--runMemo1 :: (Eq a, Hashable a) => MemoVar1 u a b -> a -> GenHaxl u b-runMemo1 (MemoVar1 r) k = unsafeLiftIO (readIORef r) >>= \case- MemoEmpty1 -> throw $ CriticalError "Attempting to run empty memo."- MemoTbl1 (f, h) -> case HashMap.lookup k h of- Nothing -> do- x <- newMemoWith (f k)- unsafeLiftIO $ writeIORef r (MemoTbl1 (f, HashMap.insert k x h))- runMemo x- Just v -> runMemo v--newMemo2 :: GenHaxl u (MemoVar2 u a b c)-newMemo2 = unsafeLiftIO $ MemoVar2 <$> newIORef MemoEmpty2--newMemoWith2 :: (a -> b -> GenHaxl u c) -> GenHaxl u (MemoVar2 u a b c)-newMemoWith2 f = newMemo2 >>= \r -> prepareMemo2 r f >> return r--prepareMemo2 :: MemoVar2 u a b c -> (a -> b -> GenHaxl u c) -> GenHaxl u ()-prepareMemo2 (MemoVar2 r) f- = unsafeLiftIO $ writeIORef r (MemoTbl2 (f, HashMap.empty))--runMemo2 :: (Eq a, Hashable a, Eq b, Hashable b)- => MemoVar2 u a b c- -> a -> b -> GenHaxl u c-runMemo2 (MemoVar2 r) k1 k2 = unsafeLiftIO (readIORef r) >>= \case- MemoEmpty2 -> throw $ CriticalError "Attempting to run empty memo."- MemoTbl2 (f, h1) -> case HashMap.lookup k1 h1 of- Nothing -> do- v <- newMemoWith (f k1 k2)- unsafeLiftIO $ writeIORef r- (MemoTbl2 (f, HashMap.insert k1 (HashMap.singleton k2 v) h1))- runMemo v- Just h2 -> case HashMap.lookup k2 h2 of- Nothing -> do- v <- newMemoWith (f k1 k2)- unsafeLiftIO $ writeIORef r- (MemoTbl2 (f, HashMap.insert k1 (HashMap.insert k2 v h2) h1))- runMemo v- Just v -> runMemo v----- -------------------------------------------------------------------------------- Parallel operations---- Bind more tightly than .&&, .||-infixr 5 `pAnd`-infixr 4 `pOr`---- | Parallel version of '(.||)'. Both arguments are evaluated in--- parallel, and if either returns 'True' then the other is--- not evaluated any further.------ WARNING: exceptions may be unpredictable when using 'pOr'. If one--- argument returns 'True' before the other completes, then 'pOr'--- returns 'True' immediately, ignoring a possible exception that--- the other argument may have produced if it had been allowed to--- complete.-pOr :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool-GenHaxl a `pOr` GenHaxl b = GenHaxl $ \env ref -> do- ra <- a env ref- case ra of- Done True -> return (Done True)- Done False -> b env ref- Throw _ -> return ra- Blocked a' -> do- rb <- b env ref- case rb of- Done True -> return (Blocked (Cont (return True)))- -- Note [tricky pOr/pAnd]- Done False -> return ra- Throw e -> return (Blocked (Cont (throw e)))- Blocked b' -> return (Blocked (Cont (toHaxl a' `pOr` toHaxl b')))---- | Parallel version of '(.&&)'. Both arguments are evaluated in--- parallel, and if either returns 'False' then the other is--- not evaluated any further.------ WARNING: exceptions may be unpredictable when using 'pAnd'. If one--- argument returns 'False' before the other completes, then 'pAnd'--- returns 'False' immediately, ignoring a possible exception that--- the other argument may have produced if it had been allowed to--- complete.-pAnd :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool-GenHaxl a `pAnd` GenHaxl b = GenHaxl $ \env ref -> do- ra <- a env ref- case ra of- Done False -> return (Done False)- Done True -> b env ref- Throw _ -> return ra- Blocked a' -> do- rb <- b env ref- case rb of- Done False -> return (Blocked (Cont (return False)))- -- Note [tricky pOr/pAnd]- Done True -> return ra- Throw _ -> return rb- Blocked b' -> return (Blocked (Cont (toHaxl a' `pAnd` toHaxl b')))--{--Note [tricky pOr/pAnd]--If one branch returns (Done True) and the other returns (Blocked _),-even though we know the result will be True (in the case of pOr), we-must return Blocked. This is because there are data fetches to-perform, and if we don't do this, the cache is left with an empty-ResultVar, and the next fetch for the same request will fail.--Alternatives:-- * Test for a non-empty RequestStore in runHaxl when we get Done, but- that would penalise every runHaxl.-- * Try to abandon the fetches. This is hard: we've already stored the- requests and a ResultVars in the cache, and we don't know how to- find the right fetches to remove from the cache. Furthermore, we- might have partially computed some memoized computations.--}+-- found in the LICENSE file.++{- TODO++- do EVENTLOG stuff, track the data fetch numbers for performFetch++- timing: we should be using clock_gettime(CLOCK_MONOTONIC) instead of+ getCurrentTime, which will be affected by NTP and leap seconds.++- write different scheduling policies++-}++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TupleSections #-}++-- |+-- The implementation of the 'Haxl' monad. Most users should+-- import "Haxl.Core" instead of importing this module directly.+--+module Haxl.Core.Monad+ (+ -- * The monad+ GenHaxl(..)+ , Result(..)++ -- * Cont+ , Cont(..)+ , toHaxl++ -- * IVar+ , IVar(..)+ , IVarContents(..)+ , newIVar+ , newFullIVar+ , getIVar+ , putIVar++ -- * ResultVal+ , ResultVal(..)+ , done+ , eitherToResult+ , eitherToResultThrowIO++ -- * CompleteReq+ , CompleteReq(..)++ -- * Env+ , Env(..)+ , Caches+ , caches+ , initEnvWithData+ , initEnv+ , emptyEnv+ , env, withEnv+ , speculate+ , imperative++ -- * JobList+ , JobList(..)+ , appendJobList+ , lengthJobList+ , addJob++ -- * Exceptions+ , throw+ , raise+ , catch+ , catchIf+ , try+ , tryToHaxlException++ -- * Dumping the cache+ , dumpCacheAsHaskell+ , dumpCacheAsHaskellFn++ -- * Unsafe operations+ , unsafeLiftIO, unsafeToHaxlException+ ) where++import Haxl.Core.Flags+import Haxl.Core.Stats+import Haxl.Core.StateStore+import Haxl.Core.Exception+import Haxl.Core.RequestStore as RequestStore+import Haxl.Core.DataCache as DataCache++import Control.Arrow (left)+import Control.Concurrent.STM+import qualified Data.Text as Text+import qualified Control.Monad.Catch as Catch+import Control.Exception (Exception(..), SomeException, throwIO)+import Control.Monad+import qualified Control.Exception as Exception+#if __GLASGOW_HASKELL__ < 710+import Control.Applicative hiding (Const)+#endif+#if __GLASGOW_HASKELL__ < 706+import Prelude hiding (catch)+#endif+import Data.IORef+import Data.Int+import GHC.Exts (IsString(..))+import Text.PrettyPrint hiding ((<>))+import Text.Printf+#ifdef EVENTLOG+import Control.Exception (bracket_)+import Debug.Trace (traceEventIO)+#endif++#ifdef PROFILING+import GHC.Stack+#endif+++trace_ :: String -> a -> a+trace_ _ = id+--trace_ = Debug.Trace.trace++-- -----------------------------------------------------------------------------+-- The environment++-- | The data we carry around in the Haxl monad.+data Env u = Env+ { cacheRef :: {-# UNPACK #-} !(IORef (DataCache (IVar u)))+ -- ^ cached data fetches++ , memoRef :: {-# UNPACK #-} !(IORef (DataCache (IVar u)))+ -- ^ memoized computations++ , flags :: !Flags+ -- conservatively not unpacking, because this is passed+ -- to 'fetch' and would need to be rebuilt.++ , userEnv :: u+ -- ^ user-supplied data, retrievable with 'env'++ , statsRef :: {-# UNPACK #-} !(IORef Stats)+ -- ^ statistics, collected according to the 'report' level in 'flags'.++ , profLabel :: ProfileLabel+ -- ^ current profiling label, see 'withLabel'++ , profRef :: {-# UNPACK #-} !(IORef Profile)+ -- ^ profiling data, collected according to the 'report' level in 'flags'.++ , states :: StateStore+ -- ^ Data sources and other components can store their state in+ -- here. Items in this store must be instances of 'StateKey'.++ , reqStoreRef :: {-# UNPACK #-} !(IORef (RequestStore u))+ -- ^ The set of requests that we have not submitted to data sources yet.+ -- Owned by the scheduler.++ , runQueueRef :: {-# UNPACK #-} !(IORef (JobList u))+ -- ^ runnable computations. Things get added to here when we wake up+ -- a computation that was waiting for something. When the list is+ -- empty, either we're finished, or we're waiting for some data fetch+ -- to return.++ , completions :: {-# UNPACK #-} !(TVar [CompleteReq u])+ -- ^ Requests that have completed. Modified by data sources+ -- (via putResult) and the scheduler. Waiting for this list to+ -- become non-empty is how the scheduler blocks waiting for+ -- data fetches to return.++ , pendingWaits :: [IO ()]+ -- ^ this is a list of IO actions returned by 'FutureFetch'+ -- data sources. These do a blocking wait for the results of+ -- some data fetch.++ , speculative :: {-# UNPACK #-} !Int+ }++type Caches u = (IORef (DataCache (IVar u)), IORef (DataCache (IVar u)))++caches :: Env u -> Caches u+caches env = (cacheRef env, memoRef env)++-- | Initialize an environment with a 'StateStore', an input map, a+-- preexisting 'DataCache', and a seed for the random number generator.+initEnvWithData :: StateStore -> u -> Caches u -> IO (Env u)+initEnvWithData states e (cref, mref) = do+ sref <- newIORef emptyStats+ pref <- newIORef emptyProfile+ rs <- newIORef noRequests -- RequestStore+ rq <- newIORef JobNil+ comps <- newTVarIO [] -- completion queue+ return Env+ { cacheRef = cref+ , memoRef = mref+ , flags = defaultFlags+ , userEnv = e+ , states = states+ , statsRef = sref+ , profLabel = "MAIN"+ , profRef = pref+ , reqStoreRef = rs+ , runQueueRef = rq+ , completions = comps+ , pendingWaits = []+ , speculative = 0+ }++-- | Initializes an environment with 'StateStore' and an input map.+initEnv :: StateStore -> u -> IO (Env u)+initEnv states e = do+ cref <- newIORef emptyDataCache+ mref <- newIORef emptyDataCache+ initEnvWithData states e (cref,mref)++-- | A new, empty environment.+emptyEnv :: u -> IO (Env u)+emptyEnv = initEnv stateEmpty++speculate :: Env u -> Env u+speculate env@Env{..}+ | speculative == 0 = env { speculative = 1 }+ | otherwise = env++imperative :: Env u -> Env u+imperative env@Env{..}+ | speculative == 1 = env { speculative = 0 }+ | otherwise = env++-- -----------------------------------------------------------------------------+-- | The Haxl monad, which does several things:+--+-- * It is a reader monad for 'Env', which contains the current state+-- of the scheduler, including unfetched requests and the run queue+-- of computations.+--+-- * It is a concurrency, or resumption, monad. A computation may run+-- partially and return 'Blocked', in which case the framework should+-- perform the outstanding requests in the 'RequestStore', and then+-- resume the computation.+--+-- * The Applicative combinator '<*>' explores /both/ branches in the+-- event that the left branch is 'Blocked', so that we can collect+-- multiple requests and submit them as a batch.+--+-- * It contains IO, so that we can perform real data fetching.+--+newtype GenHaxl u a = GenHaxl+ { unHaxl :: Env u -> IO (Result u a) }+++instance IsString a => IsString (GenHaxl u a) where+ fromString s = return (fromString s)++-- -----------------------------------------------------------------------------+-- JobList++-- | A list of computations together with the IVar into which they+-- should put their result.+--+-- This could be an ordinary list, but the optimised representation+-- saves space and time.+--+data JobList u+ = JobNil+ | forall a . JobCons+ (Env u) -- See Note [make withEnv work] below.+ (GenHaxl u a)+ {-# UNPACK #-} !(IVar u a)+ (JobList u)++-- Note [make withEnv work]+--+-- The withEnv operation supplies a new Env for the scope of a GenHaxl+-- computation. The problem is that the computation might be split+-- into pieces and put onto various JobLists, so we have to be sure to+-- use the correct Env when we execute the pieces. Furthermore, if one+-- of these pieces blocks and gets run again later, we must ensure to+-- restart it with the correct Env. So we stash the Env along with+-- the continuation in the JobList.++appendJobList :: JobList u -> JobList u -> JobList u+appendJobList JobNil c = c+appendJobList c JobNil = c+appendJobList (JobCons a b c d) e = JobCons a b c $! appendJobList d e++lengthJobList :: JobList u -> Int+lengthJobList JobNil = 0+lengthJobList (JobCons _ _ _ j) = 1 + lengthJobList j+++-- -----------------------------------------------------------------------------+-- IVar++-- | A synchronisation point. It either contains a value, or a list+-- of computations waiting for the value.+newtype IVar u a = IVar (IORef (IVarContents u a))++data IVarContents u a+ = IVarFull (ResultVal a)+ | IVarEmpty (JobList u)+ -- morally this is a list of @a -> GenHaxl u ()@, but instead of+ -- using a function, each computation begins with `getIVar` to grab+ -- the value it is waiting for. This is less type safe but a little+ -- faster (benchmarked with tests/MonadBench.hs).++newIVar :: IO (IVar u a)+newIVar = IVar <$> newIORef (IVarEmpty JobNil)++newFullIVar :: ResultVal a -> IO (IVar u a)+newFullIVar r = IVar <$> newIORef (IVarFull r)++getIVar :: IVar u a -> GenHaxl u a+getIVar (IVar !ref) = GenHaxl $ \_env -> do+ e <- readIORef ref+ case e of+ IVarFull (Ok a) -> return (Done a)+ IVarFull (ThrowHaxl e) -> return (Throw e)+ IVarFull (ThrowIO e) -> throwIO e+ IVarEmpty _ -> return (Blocked (IVar ref) (Cont (getIVar (IVar ref))))++-- Just a specialised version of getIVar, for efficiency in <*>+getIVarApply :: IVar u (a -> b) -> a -> GenHaxl u b+getIVarApply (IVar !ref) a = GenHaxl $ \_env -> do+ e <- readIORef ref+ case e of+ IVarFull (Ok f) -> return (Done (f a))+ IVarFull (ThrowHaxl e) -> return (Throw e)+ IVarFull (ThrowIO e) -> throwIO e+ IVarEmpty _ ->+ return (Blocked (IVar ref) (Cont (getIVarApply (IVar ref) a)))++putIVar :: IVar u a -> ResultVal a -> Env u -> IO ()+putIVar (IVar ref) a Env{..} = do+ e <- readIORef ref+ case e of+ IVarEmpty jobs -> do+ writeIORef ref (IVarFull a)+ modifyIORef' runQueueRef (appendJobList jobs)+ IVarFull{} -> error "putIVar: multiple put"++{-# INLINE addJob #-}+addJob :: Env u -> GenHaxl u b -> IVar u b -> IVar u a -> IO ()+addJob env !haxl !resultIVar (IVar !ref) =+ modifyIORef' ref $ \contents ->+ case contents of+ IVarEmpty list -> IVarEmpty (JobCons env haxl resultIVar list)+ _ -> addJobPanic++addJobPanic :: forall a . a+addJobPanic = error "addJob: not empty"+++-- -----------------------------------------------------------------------------+-- ResultVal++-- | The contents of a full IVar. We have to distinguish exceptions+-- thrown in the IO monad from exceptions thrown in the Haxl monad, so+-- that when the result is fetched using getIVar, we can throw the+-- exception in the right way.+data ResultVal a+ = Ok a+ | ThrowHaxl SomeException+ | ThrowIO SomeException++done :: ResultVal a -> IO (Result u a)+done (Ok a) = return (Done a)+done (ThrowHaxl e) = return (Throw e)+done (ThrowIO e) = throwIO e++eitherToResultThrowIO :: Either SomeException a -> ResultVal a+eitherToResultThrowIO (Right a) = Ok a+eitherToResultThrowIO (Left e)+ | Just HaxlException{} <- fromException e = ThrowHaxl e+ | otherwise = ThrowIO e++eitherToResult :: Either SomeException a -> ResultVal a+eitherToResult (Right a) = Ok a+eitherToResult (Left e) = ThrowHaxl e+++-- -----------------------------------------------------------------------------+-- CompleteReq++-- | A completed request from a data source, containing the result,+-- and the 'IVar' representing the blocked computations. The job of a+-- data source is just to add these to a queue ('completions') using+-- 'putResult'; the scheduler collects them from the queue and unblocks+-- the relevant computations.+data CompleteReq u+ = forall a . CompleteReq+ (Either SomeException a)+ !(IVar u a) -- IVar because the result is cached+ {-# UNPACK #-} !Int64 -- see Note [tracking allocation in child threads]+++{- Note [tracking allocation in child threads]++For a BackgroundFetch, we might be doing some of the work in a+separate thread, but we want to make sure that the parent thread gets+charged for the allocation, so that allocation limits still work.++The design is a bit tricky here. We want to track the allocation+accurately but without adding much overhead.++The best way to propagate the allocation back from the child thread is+through putResult. If we had some other method, we would also need a+way to synchronise it with the main runHaxl loop; the advantage of+putResult is that this is already a synchronisation method, because+runHaxl is waiting for the result of the dataFetch.++(slight wrinkle here: runHaxl might not wait for the result of the+dataFetch in the case where we do some speculative execution in+pAnd/pOr)++We need a special version of putResult for child threads+(putResultFromChildThread), because we don't want to propagate any+allocation from the runHaxl thread back to itself and count it twice.++We also want to capture the allocation as late as possible, so that we+count everything. For that reason, we pass a Bool down from putResult+into the function in the ResultVar, and it reads the allocation+counter as the last thing before adding the result to the completions+TVar.++The other problem to consider is how to capture the allocation when+the child thread is doing multiple putResults. Our solution here is+to ensure that the *last* one is a putResultFromChildThread, so it+captures all the allocation from everything leading up to it.++Why not reset the counter each time, so we could do multiple+putResultFromChildThreads? Because the child thread might be using an+allocation limit itself, and changing the counter would mess it up.+-}++-- -----------------------------------------------------------------------------+-- Result++-- | The result of a computation is either 'Done' with a value, 'Throw'+-- with an exception, or 'Blocked' on the result of a data fetch with+-- a continuation.+data Result u a+ = Done a+ | Throw SomeException+ | forall b . Blocked+ {-# UNPACK #-} !(IVar u b)+ (Cont u a)+ -- ^ The 'IVar' is what we are blocked on; 'Cont' is the+ -- continuation. This might be wrapped further if we're+ -- nested inside multiple '>>=', before finally being added+ -- to the 'IVar'. Morally @b -> GenHaxl u a@, but see+ -- 'IVar',++instance (Show a) => Show (Result u a) where+ show (Done a) = printf "Done(%s)" $ show a+ show (Throw e) = printf "Throw(%s)" $ show e+ show Blocked{} = "Blocked"++{- Note [Exception]++How do we want to represent Haxl exceptions (those that are thrown by+"throw" in the Haxl monad)?++1) Explicitly via a Throw constructor in the Result type+2) Using throwIO in the IO monad++If we did (2), we would have to use an exception handler in <*>,+because an exception in the right-hand argument of <*> should not+necessarily be thrown by the whole computation - an exception on the+left should get priority, and the left might currently be Blocked.++We must be careful about turning IO monad exceptions into Haxl+exceptions. An IO monad exception will normally propagate right+out of runHaxl and terminate the whole computation, whereas a Haxl+exception can get dropped on the floor, if it is on the right of+<*> and the left side also throws, for example. So turning an IO+monad exception into a Haxl exception is a dangerous thing to do.+In particular, we never want to do it for an asynchronous exception+(AllocationLimitExceeded, ThreadKilled, etc.), because these are+supposed to unconditionally terminate the computation.++There are three places where we take an arbitrary IO monad exception and+turn it into a Haxl exception:++ * wrapFetchInCatch. Here we want to propagate a failure of the+ data source to the callers of the data source, but if the+ failure came from elsewhere (an asynchronous exception), then we+ should just propagate it++ * cacheResult (cache the results of IO operations): again,+ failures of the IO operation should be visible to the caller as+ a Haxl exception, but we exclude asynchronous exceptions from+ this.++ * unsafeToHaxlException: assume the caller knows what they're+ doing, and just wrap all exceptions.+-}+++-- -----------------------------------------------------------------------------+-- Cont++-- | A data representation of a Haxl continuation. This is to avoid+-- repeatedly traversing a left-biased tree in a continuation, leading+-- O(n^2) complexity for some pathalogical cases - see the "seql" benchmark+-- in tests/MonadBench.hs.+-- See "A Smart View on Datatypes", Jaskelioff/Rivas, ICFP'15+data Cont u a+ = Cont (GenHaxl u a)+ | forall b. Cont u b :>>= (b -> GenHaxl u a)+ | forall b. (b -> a) :<$> (Cont u b)++toHaxl :: Cont u a -> GenHaxl u a+toHaxl (Cont haxl) = haxl+toHaxl (m :>>= k) = toHaxlBind m k+toHaxl (f :<$> x) = toHaxlFmap f x++toHaxlBind :: Cont u b -> (b -> GenHaxl u a) -> GenHaxl u a+toHaxlBind (m :>>= k) k2 = toHaxlBind m (k >=> k2)+toHaxlBind (Cont haxl) k = haxl >>= k+toHaxlBind (f :<$> x) k = toHaxlBind x (k . f)++toHaxlFmap :: (a -> b) -> Cont u a -> GenHaxl u b+toHaxlFmap f (m :>>= k) = toHaxlBind m (k >=> return . f)+toHaxlFmap f (Cont haxl) = f <$> haxl+toHaxlFmap f (g :<$> x) = toHaxlFmap (f . g) x+++-- -----------------------------------------------------------------------------+-- Monad/Applicative instances++instance Monad (GenHaxl u) where+ return a = GenHaxl $ \_env -> return (Done a)+ GenHaxl m >>= k = GenHaxl $ \env -> do+ e <- m env+ case e of+ Done a -> unHaxl (k a) env+ Throw e -> return (Throw e)+ Blocked ivar cont -> trace_ ">>= Blocked" $+ return (Blocked ivar (cont :>>= k))+ fail msg = GenHaxl $ \_env ->+ return $ Throw $ toException $ MonadFail $ Text.pack msg++ -- We really want the Applicative version of >>+ (>>) = (*>)++instance Functor (GenHaxl u) where+ fmap f (GenHaxl m) = GenHaxl $ \env -> do+ r <- m env+ case r of+ Done a -> return (Done (f a))+ Throw e -> return (Throw e)+ Blocked ivar cont -> trace_ "fmap Blocked" $+ return (Blocked ivar (f :<$> cont))++instance Applicative (GenHaxl u) where+ pure = return+ GenHaxl ff <*> GenHaxl aa = GenHaxl $ \env -> do+ rf <- ff env+ case rf of+ Done f -> do+ ra <- aa env+ case ra of+ Done a -> trace_ "Done/Done" $ return (Done (f a))+ Throw e -> trace_ "Done/Throw" $ return (Throw e)+ Blocked ivar fcont -> trace_ "Done/Blocked" $+ return (Blocked ivar (f :<$> fcont))+ Throw e -> trace_ "Throw" $ return (Throw e)+ Blocked ivar1 fcont -> do+ ra <- aa env+ case ra of+ Done a -> trace_ "Blocked/Done" $+ return (Blocked ivar1 (($ a) :<$> fcont))+ Throw e -> trace_ "Blocked/Throw" $+ return (Blocked ivar1 (fcont :>>= (\_ -> throw e)))+ Blocked ivar2 acont -> trace_ "Blocked/Blocked" $ do+ -- Note [Blocked/Blocked]+ if speculative env /= 0+ then+ return (Blocked ivar1+ (Cont (toHaxl fcont <*> toHaxl acont)))+ else do+ i <- newIVar+ addJob env (toHaxl fcont) i ivar1+ let cont = acont :>>= \a -> getIVarApply i a+ return (Blocked ivar2 cont)++-- Note [Blocked/Blocked]+--+-- This is the tricky case: we're blocked on both sides of the <*>.+-- We need to divide the computation into two pieces that may continue+-- independently when the resources they are blocked on become+-- available. Moreover, the computation as a whole depends on the two+-- pieces. It works like this:+--+-- ff <*> aa+--+-- becomes+--+-- (ff >>= putIVar i) <*> (a <- aa; f <- getIVar i; return (f a)+--+-- where the IVar i is a new synchronisation point. If the right side+-- gets to the `getIVar` first, it will block until the left side has+-- called 'putIVar'.+--+-- We can also do it the other way around:+--+-- (do ff <- f; getIVar i; return (ff a)) <*> (a >>= putIVar i)+--+-- The first was slightly faster according to tests/MonadBench.hs.++++-- -----------------------------------------------------------------------------+-- Env utils++-- | Extracts data from the 'Env'.+env :: (Env u -> a) -> GenHaxl u a+env f = GenHaxl $ \env -> return (Done (f env))++-- | Returns a version of the Haxl computation which always uses the+-- provided 'Env', ignoring the one specified by 'runHaxl'.+withEnv :: Env u -> GenHaxl u a -> GenHaxl u a+withEnv newEnv (GenHaxl m) = GenHaxl $ \_env -> do+ r <- m newEnv+ case r of+ Done a -> return (Done a)+ Throw e -> return (Throw e)+ Blocked ivar k ->+ return (Blocked ivar (Cont (withEnv newEnv (toHaxl k))))+++-- -----------------------------------------------------------------------------+-- Exceptions++-- | Throw an exception in the Haxl monad+throw :: (Exception e) => e -> GenHaxl u a+throw e = GenHaxl $ \_env -> raise e++raise :: (Exception e) => e -> IO (Result u a)+raise e+#ifdef PROFILING+ | Just (HaxlException Nothing h) <- fromException somex = do+ stk <- currentCallStack+ return (Throw (toException (HaxlException (Just stk) h)))+ | otherwise+#endif+ = return (Throw somex)+ where+ somex = toException e++-- | Catch an exception in the Haxl monad+catch :: Exception e => GenHaxl u a -> (e -> GenHaxl u a) -> GenHaxl u a+catch (GenHaxl m) h = GenHaxl $ \env -> do+ r <- m env+ case r of+ Done a -> return (Done a)+ Throw e | Just e' <- fromException e -> unHaxl (h e') env+ | otherwise -> return (Throw e)+ Blocked ivar k -> return (Blocked ivar (Cont (catch (toHaxl k) h)))++-- | Catch exceptions that satisfy a predicate+catchIf+ :: Exception e => (e -> Bool) -> GenHaxl u a -> (e -> GenHaxl u a)+ -> GenHaxl u a+catchIf cond haxl handler =+ catch haxl $ \e -> if cond e then handler e else throw e++-- | Returns @'Left' e@ if the computation throws an exception @e@, or+-- @'Right' a@ if it returns a result @a@.+try :: Exception e => GenHaxl u a -> GenHaxl u (Either e a)+try haxl = (Right <$> haxl) `catch` (return . Left)++-- | @since 0.3.1.0+instance Catch.MonadThrow (GenHaxl u) where throwM = Haxl.Core.Monad.throw+-- | @since 0.3.1.0+instance Catch.MonadCatch (GenHaxl u) where catch = Haxl.Core.Monad.catch+++-- -----------------------------------------------------------------------------+-- Unsafe operations++-- | Under ordinary circumstances this is unnecessary; users of the Haxl+-- monad should generally /not/ perform arbitrary IO.+unsafeLiftIO :: IO a -> GenHaxl u a+unsafeLiftIO m = GenHaxl $ \_env -> Done <$> m++-- | Convert exceptions in the underlying IO monad to exceptions in+-- the Haxl monad. This is morally unsafe, because you could then+-- catch those exceptions in Haxl and observe the underlying execution+-- order. Not to be exposed to user code.+unsafeToHaxlException :: GenHaxl u a -> GenHaxl u a+unsafeToHaxlException (GenHaxl m) = GenHaxl $ \env -> do+ r <- m env `Exception.catch` \e -> return (Throw e)+ case r of+ Blocked cvar c ->+ return (Blocked cvar (Cont (unsafeToHaxlException (toHaxl c))))+ other -> return other++-- | Like 'try', but lifts all exceptions into the 'HaxlException'+-- hierarchy. Uses 'unsafeToHaxlException' internally. Typically+-- this is used at the top level of a Haxl computation, to ensure that+-- all exceptions are caught.+tryToHaxlException :: GenHaxl u a -> GenHaxl u (Either HaxlException a)+tryToHaxlException h = left asHaxlException <$> try (unsafeToHaxlException h)+++-- -----------------------------------------------------------------------------++-- | Dump the contents of the cache as Haskell code that, when+-- compiled and run, will recreate the same cache contents. For+-- example, the generated code looks something like this:+--+-- > loadCache :: GenHaxl u ()+-- > loadCache = do+-- > cacheRequest (ListWombats 3) (Right ([1,2,3]))+-- > cacheRequest (CountAardvarks "abcabc") (Right (2))+--+dumpCacheAsHaskell :: GenHaxl u String+dumpCacheAsHaskell = dumpCacheAsHaskellFn "loadCache" "GenHaxl u ()"++-- | Dump the contents of the cache as Haskell code that, when+-- compiled and run, will recreate the same cache contents.+--+-- Takes the name and type for the resulting function as arguments.+dumpCacheAsHaskellFn :: String -> String -> GenHaxl u String+dumpCacheAsHaskellFn fnName fnType = do+ ref <- env cacheRef -- NB. cacheRef, not memoRef. We ignore memoized+ -- results when dumping the cache.+ let+ readIVar (IVar ref) = do+ r <- readIORef ref+ case r of+ IVarFull (Ok a) -> return (Just (Right a))+ IVarFull (ThrowHaxl e) -> return (Just (Left e))+ IVarFull (ThrowIO e) -> return (Just (Left e))+ IVarEmpty _ -> return Nothing++ mk_cr (req, res) =+ text "cacheRequest" <+> parens (text req) <+> parens (result res)+ result (Left e) = text "except" <+> parens (text (show e))+ result (Right s) = text "Right" <+> parens (text s)++ entries <- unsafeLiftIO $ do+ cache <- readIORef ref+ showCache cache readIVar++ return $ show $+ text (fnName ++ " :: " ++ fnType) $$+ text (fnName ++ " = do") $$+ nest 2 (vcat (map mk_cr (concatMap snd entries))) $$+ text "" -- final newline
+ Haxl/Core/Parallel.hs view
@@ -0,0 +1,84 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RecordWildCards #-}++-- | Psuedo-parallel operations. Most users should import "Haxl.Core"+-- instead.+--+module Haxl.Core.Parallel+ ( -- * Parallel operations+ pAnd+ , pOr+ ) where++import Haxl.Core.Monad++-- -----------------------------------------------------------------------------+-- Parallel operations++-- Bind more tightly than .&&, .||+infixr 5 `pAnd`+infixr 4 `pOr`++-- | Parallel version of '(.||)'. Both arguments are evaluated in+-- parallel, and if either returns 'True' then the other is+-- not evaluated any further.+--+-- WARNING: exceptions may be unpredictable when using 'pOr'. If one+-- argument returns 'True' before the other completes, then 'pOr'+-- returns 'True' immediately, ignoring a possible exception that+-- the other argument may have produced if it had been allowed to+-- complete.+pOr :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool+GenHaxl a `pOr` GenHaxl b = GenHaxl $ \env@Env{..} -> do+ let !senv = speculate env+ ra <- a senv+ case ra of+ Done True -> return (Done True)+ Done False -> b env -- not speculative+ Throw _ -> return ra+ Blocked ia a' -> do+ rb <- b senv+ case rb of+ Done True -> return rb+ Done False -> return ra+ Throw _ -> return rb+ Blocked _ b' -> return (Blocked ia (Cont (toHaxl a' `pOr` toHaxl b')))+ -- Note [pOr Blocked/Blocked]+ -- This will only wake up when ia is filled, which+ -- is whatever the left side was waiting for. This is+ -- suboptimal because the right side might wake up first,+ -- but handling this non-determinism would involve a much+ -- more complicated implementation here.++-- | Parallel version of '(.&&)'. Both arguments are evaluated in+-- parallel, and if either returns 'False' then the other is+-- not evaluated any further.+--+-- WARNING: exceptions may be unpredictable when using 'pAnd'. If one+-- argument returns 'False' before the other completes, then 'pAnd'+-- returns 'False' immediately, ignoring a possible exception that+-- the other argument may have produced if it had been allowed to+-- complete.+pAnd :: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool+GenHaxl a `pAnd` GenHaxl b = GenHaxl $ \env@Env{..} -> do+ let !senv = speculate env+ ra <- a senv+ case ra of+ Done False -> return (Done False)+ Done True -> b env+ Throw _ -> return ra+ Blocked ia a' -> do+ rb <- b senv+ case rb of+ Done False -> return rb+ Done True -> return ra+ Throw _ -> return rb+ Blocked _ b' -> return (Blocked ia (Cont (toHaxl a' `pAnd` toHaxl b')))+ -- See Note [pOr Blocked/Blocked]
+ Haxl/Core/Profile.hs view
@@ -0,0 +1,158 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE MagicHash #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Implementation of lightweight profiling. Most users should+-- import "Haxl.Core" instead.+--+module Haxl.Core.Profile+ ( withLabel+ , withFingerprintLabel+ , addProfileFetch+ , incrementMemoHitCounterFor+ , collectProfileData+ , profileCont+ ) where++import Data.IORef+import Data.Hashable+import Data.Monoid+import Data.Text (Text)+import Data.Typeable+import qualified Data.HashMap.Strict as HashMap+import qualified Data.HashSet as HashSet+import GHC.Exts+import qualified Data.Text as Text++import Haxl.Core.DataSource+import Haxl.Core.Flags+import Haxl.Core.Stats+import Haxl.Core.Monad++-- -----------------------------------------------------------------------------+-- Profiling++-- | Label a computation so profiling data is attributed to the label.+withLabel :: ProfileLabel -> GenHaxl u a -> GenHaxl u a+withLabel l (GenHaxl m) = GenHaxl $ \env ->+ if report (flags env) < 4+ then m env+ else collectProfileData l m env++-- | Label a computation so profiling data is attributed to the label.+-- Intended only for internal use by 'memoFingerprint'.+withFingerprintLabel :: Addr# -> Addr# -> GenHaxl u a -> GenHaxl u a+withFingerprintLabel mnPtr nPtr (GenHaxl m) = GenHaxl $ \env ->+ if report (flags env) < 4+ then m env+ else collectProfileData+ (Text.unpackCString# mnPtr <> "." <> Text.unpackCString# nPtr)+ m env++-- | Collect profiling data and attribute it to given label.+collectProfileData+ :: ProfileLabel+ -> (Env u -> IO (Result u a))+ -> Env u+ -> IO (Result u a)+collectProfileData l m env = do+ a0 <- getAllocationCounter+ r <- m env{profLabel=l} -- what if it throws?+ a1 <- getAllocationCounter+ modifyProfileData env l (a0 - a1)+ -- So we do not count the allocation overhead of modifyProfileData+ setAllocationCounter a1+ case r of+ Done a -> return (Done a)+ Throw e -> return (Throw e)+ Blocked ivar k -> return (Blocked ivar (Cont (withLabel l (toHaxl k))))+{-# INLINE collectProfileData #-}++modifyProfileData :: Env u -> ProfileLabel -> AllocCount -> IO ()+modifyProfileData env label allocs =+ modifyIORef' (profRef env) $ \ p ->+ p { profile =+ HashMap.insertWith updEntry label newEntry .+ HashMap.insertWith updCaller caller newCaller $+ profile p }+ where caller = profLabel env+ newEntry =+ emptyProfileData+ { profileAllocs = allocs+ , profileDeps = HashSet.singleton caller }+ updEntry _ old =+ old { profileAllocs = profileAllocs old + allocs+ , profileDeps = HashSet.insert caller (profileDeps old) }+ -- subtract allocs from caller, so they are not double counted+ -- we don't know the caller's caller, but it will get set on+ -- the way back out, so an empty hashset is fine for now+ newCaller =+ emptyProfileData { profileAllocs = -allocs }+ updCaller _ old =+ old { profileAllocs = profileAllocs old - allocs }+++-- Like collectProfileData, but intended to be run from the scheduler.+--+-- * doesn't add a dependency (the original withLabel did this)+--+-- * doesn't subtract allocs from the caller (we're evaluating this+-- cont from the top level, so we don't need this)+--+-- * doesn't wrap a Blocked continuation in withLabel (the scheduler+-- will call profileCont the next time this cont runs)+--+profileCont+ :: (Env u -> IO (Result u a))+ -> Env u+ -> IO (Result u a)+profileCont m env = do+ a0 <- getAllocationCounter+ r <- m env+ a1 <- getAllocationCounter+ let+ allocs = a0 - a1+ newEntry = emptyProfileData { profileAllocs = allocs }+ updEntry _ old = old { profileAllocs = profileAllocs old + allocs }+ modifyIORef' (profRef env) $ \ p ->+ p { profile =+ HashMap.insertWith updEntry (profLabel env) newEntry $+ profile p }+ -- So we do not count the allocation overhead of modifyProfileData+ setAllocationCounter a1+ return r+{-# INLINE profileCont #-}+++incrementMemoHitCounterFor :: ProfileLabel -> Profile -> Profile+incrementMemoHitCounterFor lbl p =+ p { profile = HashMap.adjust incrementMemoHitCounter lbl (profile p) }++incrementMemoHitCounter :: ProfileData -> ProfileData+incrementMemoHitCounter pd = pd { profileMemoHits = succ (profileMemoHits pd) }++{-# NOINLINE addProfileFetch #-}+addProfileFetch+ :: forall r u a . (DataSourceName r, Eq (r a), Hashable (r a), Typeable (r a))+ => Env u -> r a -> IO ()+addProfileFetch env _req = do+ c <- getAllocationCounter+ modifyIORef' (profRef env) $ \ p ->+ let+ dsName :: Text+ dsName = dataSourceName (Proxy :: Proxy r)++ upd :: ProfileData -> ProfileData+ upd d = d { profileFetches =+ HashMap.insertWith (+) dsName 1 (profileFetches d) }++ in p { profile = HashMap.adjust upd (profLabel env) (profile p) }+ -- So we do not count the allocation overhead of addProfileFetch+ setAllocationCounter c
Haxl/Core/RequestStore.hs view
@@ -2,8 +2,7 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-}@@ -18,12 +17,17 @@ -- -- This module is provided for access to Haxl internals only; most -- users should not need to import it.-module Haxl.Core.RequestStore (- BlockedFetches(..), RequestStore,- noRequests, addRequest, contents+--+module Haxl.Core.RequestStore+ ( BlockedFetches(..)+ , RequestStore+ , isEmpty+ , noRequests+ , addRequest+ , contents ) where -import Haxl.Core.Types+import Haxl.Core.DataSource import Data.Map (Map) import qualified Data.Map.Strict as Map import Data.Typeable@@ -38,6 +42,9 @@ -- | A batch of 'BlockedFetch' objects for a single 'DataSource' data BlockedFetches u = forall r. (DataSource u r) => BlockedFetches [BlockedFetch r]++isEmpty :: RequestStore u -> Bool+isEmpty (RequestStore m) = Map.null m -- | A new empty 'RequestStore'. noRequests :: RequestStore u
+ Haxl/Core/Run.hs view
@@ -0,0 +1,220 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Defines 'runHaxl'. Most users should import "Haxl.Core" instead.+--+module Haxl.Core.Run+ ( runHaxl+ ) where++import Control.Concurrent.STM+import Control.Exception as Exception+import Control.Monad+import Data.IORef+import Text.Printf+import Unsafe.Coerce++import Haxl.Core.DataCache+import Haxl.Core.Exception+import Haxl.Core.Flags+import Haxl.Core.Monad+import Haxl.Core.Fetch+import Haxl.Core.Profile+import Haxl.Core.RequestStore as RequestStore+import Haxl.Core.Stats+++-- -----------------------------------------------------------------------------+-- runHaxl++-- | Runs a 'Haxl' computation in the given 'Env'.+runHaxl :: forall u a. Env u -> GenHaxl u a -> IO a+runHaxl env@Env{..} haxl = do++ result@(IVar resultRef) <- newIVar -- where to put the final result+ let+ -- Run a job, and put its result in the given IVar+ schedule :: Env u -> JobList u -> GenHaxl u b -> IVar u b -> IO ()+ schedule env@Env{..} rq (GenHaxl run) (IVar !ref) = do+ ifTrace flags 3 $ printf "schedule: %d\n" (1 + lengthJobList rq)+ let {-# INLINE result #-}+ result r = do+ e <- readIORef ref+ case e of+ IVarFull _ -> error "multiple put"+ IVarEmpty haxls -> do+ writeIORef ref (IVarFull r)+ -- Have we got the final result now?+ if ref == unsafeCoerce resultRef+ -- comparing IORefs of different types is safe, it's+ -- pointer-equality on the MutVar#.+ then+ -- We have a result, but don't discard unfinished+ -- computations in the run queue. See+ -- Note [runHaxl and unfinished requests].+ -- Nothing can depend on the final IVar, so haxls must+ -- be empty.+ case rq of+ JobNil -> return ()+ _ -> modifyIORef' runQueueRef (appendJobList rq)+ else reschedule env (appendJobList haxls rq)+ r <-+ if report flags >= 4 -- withLabel unfolded+ then Exception.try $ profileCont run env+ else Exception.try $ run env+ case r of+ Left e -> do+ rethrowAsyncExceptions e+ result (ThrowIO e)+ Right (Done a) -> result (Ok a)+ Right (Throw ex) -> result (ThrowHaxl ex)+ Right (Blocked ivar fn) -> do+ addJob env (toHaxl fn) (IVar ref) ivar+ reschedule env rq++ -- Here we have a choice:+ -- - If the requestStore is non-empty, we could submit those+ -- requests right away without waiting for more. This might+ -- be good for latency, especially if the data source doesn't+ -- support batching, or if batching is pessimal.+ -- - To optimise the batch sizes, we want to execute as much as+ -- we can and only submit requests when we have no more+ -- computation to do.+ -- - compromise: wait at least Nms for an outstanding result+ -- before giving up and submitting new requests.+ --+ -- For now we use the batching strategy in the scheduler, but+ -- individual data sources can request that their requests are+ -- sent eagerly by using schedulerHint.+ --+ reschedule :: Env u -> JobList u -> IO ()+ reschedule env@Env{..} haxls = do+ case haxls of+ JobNil -> do+ rq <- readIORef runQueueRef+ case rq of+ JobNil -> emptyRunQueue env+ JobCons env' a b c -> do+ writeIORef runQueueRef JobNil+ schedule env' c a b+ JobCons env' a b c ->+ schedule env' c a b++ emptyRunQueue :: Env u -> IO ()+ emptyRunQueue env@Env{..} = do+ ifTrace flags 3 $ printf "emptyRunQueue\n"+ haxls <- checkCompletions env+ case haxls of+ JobNil -> do+ case pendingWaits of+ [] -> checkRequestStore env+ wait:waits -> do+ ifTrace flags 3 $ printf "invoking wait\n"+ wait+ emptyRunQueue env { pendingWaits = waits } -- check completions+ _ -> reschedule env haxls++ checkRequestStore :: Env u -> IO ()+ checkRequestStore env@Env{..} = do+ reqStore <- readIORef reqStoreRef+ if RequestStore.isEmpty reqStore+ then waitCompletions env+ else do+ writeIORef reqStoreRef noRequests+ (_, waits) <- performRequestStore 0 env reqStore+ ifTrace flags 3 $ printf "performFetches: %d waits\n" (length waits)+ -- empty the cache if we're not caching. Is this the best+ -- place to do it? We do get to de-duplicate requests that+ -- happen simultaneously.+ when (caching flags == 0) $+ writeIORef cacheRef emptyDataCache+ emptyRunQueue env{ pendingWaits = waits ++ pendingWaits }++ checkCompletions :: Env u -> IO (JobList u)+ checkCompletions Env{..} = do+ ifTrace flags 3 $ printf "checkCompletions\n"+ comps <- atomically $ do+ c <- readTVar completions+ writeTVar completions []+ return c+ case comps of+ [] -> return JobNil+ _ -> do+ ifTrace flags 3 $ printf "%d complete\n" (length comps)+ let+ getComplete (CompleteReq a (IVar cr) allocs) = do+ when (allocs < 0) $ do+ cur <- getAllocationCounter+ setAllocationCounter (cur + allocs)+ r <- readIORef cr+ case r of+ IVarFull _ -> do+ ifTrace flags 3 $ printf "existing result\n"+ return JobNil+ -- this happens if a data source reports a result,+ -- and then throws an exception. We call putResult+ -- a second time for the exception, which comes+ -- ahead of the original request (because it is+ -- pushed on the front of the completions list) and+ -- therefore overrides it.+ IVarEmpty cv -> do+ writeIORef cr (IVarFull (eitherToResult a))+ return cv+ jobs <- mapM getComplete comps+ return (foldr appendJobList JobNil jobs)++ waitCompletions :: Env u -> IO ()+ waitCompletions env@Env{..} = do+ ifTrace flags 3 $ printf "waitCompletions\n"+ atomically $ do+ c <- readTVar completions+ when (null c) retry+ emptyRunQueue env++ --+ schedule env JobNil haxl result+ r <- readIORef resultRef+ case r of+ IVarEmpty _ -> throwIO (CriticalError "runHaxl: missing result")+ IVarFull (Ok a) -> return a+ IVarFull (ThrowHaxl e) -> throwIO e+ IVarFull (ThrowIO e) -> throwIO e+++{- Note [runHaxl and unfinished requests]++runHaxl returns immediately when the supplied computation has returned+a result. This doesn't necessarily mean that the whole computaiton+graph has completed, however. In particular, when using pAnd and pOr,+we might have created some data fetches that have not completed, but+weren't required, because the other branch of the pAnd/pOr subsumed+the result.++When runHaxl returns, it might be that:+- reqStoreRef contains some unsubmitted requests+- runQueueRef contains some jobs+- there are in-flight BackgroundFetch requests, that will return their+ results to the completions queue in due course.+- there are various unfilled IVars in the cache and/or memo tables++This should be all safe, we can even restart runHaxl with the same Env+after it has stopped and the in-progress computations will+continue. But don't discard the contents of+reqStoreRef/runQueueRef/completions, because then we'll deadlock if we+discover one of the unfilled IVars in the cache or memo table.+-}++{- TODO: later+data SchedPolicy+ = SubmitImmediately+ | WaitAtLeast Int{-ms-}+ | WaitForAllPendingRequests+-}
Haxl/Core/ShowP.hs view
@@ -2,11 +2,12 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. +-- | -- Most users should import "Haxl.Core" instead of importing this -- module directly.+-- module Haxl.Core.ShowP ( ShowP(..) ) where
Haxl/Core/StateStore.hs view
@@ -2,8 +2,7 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-}@@ -11,10 +10,16 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE CPP #-} --- | Most users should import "Haxl.Core" instead of importing this+-- |+-- Most users should import "Haxl.Core" instead of importing this -- module directly.-module Haxl.Core.StateStore (- StateKey(..), StateStore, stateGet, stateSet, stateEmpty+--+module Haxl.Core.StateStore+ ( StateKey(..)+ , StateStore+ , stateGet+ , stateSet+ , stateEmpty ) where import Data.Map (Map)@@ -28,12 +33,17 @@ -- #if __GLASGOW_HASKELL__ >= 708 class Typeable f => StateKey (f :: * -> *) where- data State f #else class Typeable1 f => StateKey (f :: * -> *) where- data State f #endif+ data State f + -- | We default this to typeOf1, but if f is itself a complex type that is+ -- already applied to some paramaters, we want to be able to use the same+ -- state by using typeOf2, etc+ getStateType :: Proxy f -> TypeRep+ getStateType = typeRep+ -- | The 'StateStore' maps a 'StateKey' to the 'State' for that type. newtype StateStore = StateStore (Map TypeRep StateStoreData) @@ -48,22 +58,18 @@ -- | Inserts a `State` in the `StateStore` container. stateSet :: forall f . StateKey f => State f -> StateStore -> StateStore stateSet st (StateStore m) =- StateStore (Map.insert (getType st) (StateStoreData st) m)+ StateStore (Map.insert (getStateType (Proxy :: Proxy f)) (StateStoreData st) m) -- | Retrieves a `State` from the `StateStore` container. stateGet :: forall r . StateKey r => StateStore -> Maybe (State r) stateGet (StateStore m) = case Map.lookup ty m of Nothing -> Nothing- Just (StateStoreData st)- | getType st == ty -> Just (unsafeCoerce st)- | otherwise -> Nothing+ Just (StateStoreData (st :: State f))+ | getStateType (Proxy :: Proxy f) == ty -> Just (unsafeCoerce st)+ | otherwise -> Nothing -- the dynamic type check here should be unnecessary, but if -- there are bugs in `Typeable` or `Map` then we'll get an -- error instead of a crash. The overhead is a few percent. where- ty = getType (undefined :: State r)---- | Returns the 'TypeRep' associated with a particular 'State'.-getType :: forall f . StateKey f => State f -> TypeRep-getType _ = typeOf1 (undefined :: f a)+ ty = getStateType (Proxy :: Proxy r)
+ Haxl/Core/Stats.hs view
@@ -0,0 +1,209 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Types and operations for statistics and profiling. Most users+-- should import "Haxl.Core" instead of importing this module+-- directly.+--+module Haxl.Core.Stats+ (+ -- * Data-source stats+ Stats(..)+ , FetchStats(..)+ , Microseconds+ , Timestamp+ , getTimestamp+ , emptyStats+ , numRounds+ , numFetches+ , ppStats+ , ppFetchStats++ -- * Profiling+ , Profile+ , emptyProfile+ , profile+ , ProfileLabel+ , ProfileData(..)+ , emptyProfileData+ , AllocCount+ , MemoHitCount++ -- * Allocation+ , getAllocationCounter+ , setAllocationCounter+ ) where++import Data.Aeson+import Data.HashMap.Strict (HashMap)+import Data.HashSet (HashSet)+import Data.Int+import Data.List (intercalate, maximumBy, minimumBy)+#if __GLASGOW_HASKELL__ < 710+import Data.Monoid+#endif+import Data.Semigroup (Semigroup)+import Data.Ord (comparing)+import Data.Text (Text)+import Data.Time.Clock.POSIX+import Text.Printf+import qualified Data.HashMap.Strict as HashMap+import qualified Data.HashSet as HashSet+import qualified Data.Text as Text++#if __GLASGOW_HASKELL__ >= 710+import GHC.Conc (getAllocationCounter, setAllocationCounter)+#endif++-- ---------------------------------------------------------------------------+-- Measuring time++type Microseconds = Int64+type Timestamp = Microseconds -- since an epoch++getTimestamp :: IO Timestamp+getTimestamp = do+ t <- getPOSIXTime -- for now, TODO better+ return (round (t * 1000000))++-- ---------------------------------------------------------------------------+-- Stats++-- | Stats that we collect along the way.+newtype Stats = Stats [FetchStats]+ deriving (Show, ToJSON, Semigroup, Monoid)++-- | Pretty-print Stats.+ppStats :: Stats -> String+ppStats (Stats rss) =+ intercalate "\n"+ [ "["+ ++ [+ if fetchWasRunning rs+ (minStartTime + (t - 1) * usPerDash)+ (minStartTime + t * usPerDash)+ then '*'+ else '-'+ | t <- [1..numDashes]+ ]+ ++ "] " ++ show i ++ " - " ++ ppFetchStats rs+ | (i, rs) <- zip [(1::Int)..] validFetchStats ]+ where+ isFetchStats FetchStats{} = True+ isFetchStats _ = False+ validFetchStats = filter isFetchStats (reverse rss)+ numDashes = 50+ minStartTime = fetchStart $ minimumBy (comparing fetchStart) validFetchStats+ lastFs = maximumBy (comparing (\fs -> fetchStart fs + fetchDuration fs))+ validFetchStats+ usPerDash = (fetchStart lastFs + fetchDuration lastFs - minStartTime)+ `div` numDashes+ fetchWasRunning :: FetchStats -> Timestamp -> Timestamp -> Bool+ fetchWasRunning fs t1 t2 =+ (fetchStart fs + fetchDuration fs) >= t1 && fetchStart fs < t2+++-- | Maps data source name to the number of requests made in that round.+-- The map only contains entries for sources that made requests in that+-- round.+data FetchStats+ -- | Timing stats for a (batched) data fetch+ = FetchStats+ { fetchDataSource :: Text+ , fetchBatchSize :: {-# UNPACK #-} !Int+ , fetchStart :: !Timestamp -- TODO should be something else+ , fetchDuration :: {-# UNPACK #-} !Microseconds+ , fetchSpace :: {-# UNPACK #-} !Int64+ , fetchFailures :: {-# UNPACK #-} !Int+ }++ -- | The stack trace of a call to 'dataFetch'. These are collected+ -- only when profiling and reportLevel is 5 or greater.+ | FetchCall+ { fetchReq :: String+ , fetchStack :: [String]+ }+ deriving (Show)++-- | Pretty-print RoundStats.+ppFetchStats :: FetchStats -> String+ppFetchStats FetchStats{..} =+ printf "%s: %d fetches (%.2fms, %d bytes, %d failures)"+ (Text.unpack fetchDataSource) fetchBatchSize+ (fromIntegral fetchDuration / 1000 :: Double) fetchSpace fetchFailures+ppFetchStats (FetchCall r ss) = show r ++ '\n':show ss++instance ToJSON FetchStats where+ toJSON FetchStats{..} = object+ [ "datasource" .= fetchDataSource+ , "fetches" .= fetchBatchSize+ , "start" .= fetchStart+ , "duration" .= fetchDuration+ , "allocation" .= fetchSpace+ , "failures" .= fetchFailures+ ]+ toJSON (FetchCall req strs) = object+ [ "request" .= req+ , "stack" .= strs+ ]++emptyStats :: Stats+emptyStats = Stats []++numRounds :: Stats -> Int+numRounds (Stats rs) = length rs -- not really++numFetches :: Stats -> Int+numFetches (Stats rs) = sum [ fetchBatchSize | FetchStats{..} <- rs ]+++-- ---------------------------------------------------------------------------+-- Profiling++type ProfileLabel = Text+type AllocCount = Int64+type MemoHitCount = Int64++newtype Profile = Profile+ { profile :: HashMap ProfileLabel ProfileData+ -- ^ Data on individual labels.+ }++emptyProfile :: Profile+emptyProfile = Profile HashMap.empty++data ProfileData = ProfileData+ { profileAllocs :: {-# UNPACK #-} !AllocCount+ -- ^ allocations made by this label+ , profileDeps :: HashSet ProfileLabel+ -- ^ labels that this label depends on+ , profileFetches :: HashMap Text Int+ -- ^ map from datasource name => fetch count+ , profileMemoHits :: {-# UNPACK #-} !MemoHitCount+ -- ^ number of hits to memoized computation at this label+ }+ deriving Show++emptyProfileData :: ProfileData+emptyProfileData = ProfileData 0 HashSet.empty HashMap.empty 0+++-- -----------------------------------------------------------------------------+-- Allocation accounting++#if __GLASGOW_HASKELL__ < 710+getAllocationCounter :: IO Int64+getAllocationCounter = return 0++setAllocationCounter :: Int64 -> IO ()+setAllocationCounter _ = return ()+#endif
− Haxl/Core/Types.hs
@@ -1,636 +0,0 @@--- Copyright (c) 2014-present, Facebook, Inc.--- All rights reserved.------ This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.--{-# LANGUAGE CPP #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-#if __GLASGOW_HASKELL >= 800-{-# OPTIONS_GHC -Wno-name-shadowing #-}-#else-{-# OPTIONS_GHC -fno-warn-name-shadowing #-}-#endif---- | Base types used by all of Haxl. Most users should import--- "Haxl.Core" instead of importing this module directly.-module Haxl.Core.Types (-- -- * Tracing flags- Flags(..),- defaultFlags,- ifTrace,- ifReport,- ifProfiling,-- -- * Statistics- Stats(..),- RoundStats(..),- DataSourceRoundStats(..),- Microseconds,- Round,- emptyStats,- numRounds,- numFetches,- ppStats,- ppRoundStats,- ppDataSourceRoundStats,- Profile,- emptyProfile,- profile,- profileRound,- profileCache,- ProfileLabel,- ProfileData(..),- emptyProfileData,- AllocCount,- MemoHitCount,-- -- * Data fetching- DataSource(..),- DataSourceName(..),- Request,- BlockedFetch(..),- PerformFetch(..),-- -- * DataCache- DataCache(..),- SubCache(..),- emptyDataCache,-- -- * Result variables- ResultVar(..),- newEmptyResult,- newResult,- putFailure,- putResult,- putSuccess,- takeResult,- tryReadResult,- tryTakeResult,-- -- * Default fetch implementations- asyncFetch, asyncFetchWithDispatch,- asyncFetchAcquireRelease,- stubFetch,- syncFetch,-- -- * Utilities- except,- setError,-- ) where--#if __GLASGOW_HASKELL__ < 710-import Control.Applicative-#endif-import Control.Concurrent.MVar-import Control.Exception-import Control.Monad-import Data.Aeson-import Data.Function (on)-import Data.Functor.Constant-import Data.Int-import Data.Hashable-import Data.HashMap.Strict (HashMap, toList)-import qualified Data.HashMap.Strict as HashMap-import Data.HashSet (HashSet)-import qualified Data.HashSet as HashSet-import Data.List (intercalate, sortBy)-import Data.Map (Map)-import qualified Data.Map as Map-import Data.Text (Text, unpack)-import Data.Typeable--import Haxl.Core.Exception-#if __GLASGOW_HASKELL__ < 708-import Haxl.Core.Util (tryReadMVar)-#endif-import Haxl.Core.ShowP-import Haxl.Core.StateStore---- ------------------------------------------------------------------------------ Flags---- | Flags that control the operation of the engine.-data Flags = Flags- { trace :: {-# UNPACK #-} !Int- -- ^ Tracing level (0 = quiet, 3 = very verbose).- , report :: {-# UNPACK #-} !Int- -- ^ Report level (0 = quiet, 1 = # of requests, 2 = time, 3 = # of errors,- -- 4 = profiling, 5 = log stack traces of dataFetch calls)- , caching :: {-# UNPACK #-} !Int- -- ^ Non-zero if caching is enabled. If caching is disabled, then- -- we still do batching and de-duplication within a round, but do- -- not cache results between rounds.- }--defaultFlags :: Flags-defaultFlags = Flags- { trace = 0- , report = 1- , caching = 1- }--#if __GLASGOW_HASKELL__ >= 710-#define FUNMONAD Monad m-#else-#define FUNMONAD (Functor m, Monad m)-#endif---- | Runs an action if the tracing level is above the given threshold.-ifTrace :: FUNMONAD => Flags -> Int -> m a -> m ()-ifTrace flags i = when (trace flags >= i) . void---- | Runs an action if the report level is above the given threshold.-ifReport :: FUNMONAD => Flags -> Int -> m a -> m ()-ifReport flags i = when (report flags >= i) . void--ifProfiling :: FUNMONAD => Flags -> m a -> m ()-ifProfiling flags = when (report flags >= 4) . void--#undef FUNMONAD---- ------------------------------------------------------------------------------ Stats--type Microseconds = Int--- | Rounds are 1-indexed-type Round = Int---- | Stats that we collect along the way.-newtype Stats = Stats [RoundStats]- deriving (Show, ToJSON)---- | Pretty-print Stats.-ppStats :: Stats -> String-ppStats (Stats rss) =- intercalate "\n"- [ "Round: " ++ show i ++ " - " ++ ppRoundStats rs- | (i, rs) <- zip [(1::Int)..] (filter isRoundStats (reverse rss)) ]- where- isRoundStats RoundStats{} = True- isRoundStats _ = False---- | Maps data source name to the number of requests made in that round.--- The map only contains entries for sources that made requests in that--- round.-data RoundStats- -- | Timing stats for a round of data fetching- = RoundStats- { roundTime :: Microseconds- , roundAllocation :: Int- , roundDataSources :: HashMap Text DataSourceRoundStats- }- -- | The stack trace of a call to 'dataFetch'. These are collected- -- only when profiling and reportLevel is 5 or greater.- | FetchCall- { fetchReq :: String- , fetchStack :: [String]- }- deriving (Show)---- | Pretty-print RoundStats.-ppRoundStats :: RoundStats -> String-ppRoundStats (RoundStats t a dss) =- show t ++ "us " ++ show a ++ " bytes\n"- ++ unlines [ " " ++ unpack nm ++ ": " ++ ppDataSourceRoundStats dsrs- | (nm, dsrs) <- sortBy (compare `on` fst) (toList dss) ]-ppRoundStats (FetchCall r ss) = show r ++ '\n':show ss--instance ToJSON RoundStats where- toJSON RoundStats{..} = object- [ "time" .= roundTime- , "allocation" .= roundAllocation- , "dataSources" .= roundDataSources- ]- toJSON (FetchCall req strs) = object- [ "request" .= req- , "stack" .= strs- ]---- | Detailed stats of each data source in each round.-data DataSourceRoundStats = DataSourceRoundStats- { dataSourceFetches :: Int- , dataSourceTime :: Maybe Microseconds- , dataSourceFailures :: Maybe Int- , dataSourceAllocation :: Maybe Int- } deriving (Show)---- | Pretty-print DataSourceRoundStats-ppDataSourceRoundStats :: DataSourceRoundStats -> String-ppDataSourceRoundStats (DataSourceRoundStats fetches time failures allocs) =- maybe id (\t s -> s ++ " (" ++ show t ++ "us)") time $- maybe id (\a s -> s ++ " (" ++ show a ++ " bytes)") allocs $- maybe id (\f s -> s ++ " " ++ show f ++ " failures") failures $- show fetches ++ " fetches"--instance ToJSON DataSourceRoundStats where- toJSON DataSourceRoundStats{..} = object [k .= v | (k, Just v) <-- [ ("fetches", Just dataSourceFetches)- , ("time", dataSourceTime)- , ("failures", dataSourceFailures)- , ("allocation", dataSourceAllocation)- ]]--fetchesInRound :: RoundStats -> Int-fetchesInRound (RoundStats _ _ hm) =- sum $ map dataSourceFetches $ HashMap.elems hm-fetchesInRound _ = 0--emptyStats :: Stats-emptyStats = Stats []--numRounds :: Stats -> Int-numRounds (Stats rs) = length [ s | s@RoundStats{} <- rs ]--numFetches :: Stats -> Int-numFetches (Stats rs) = sum (map fetchesInRound rs)----- ------------------------------------------------------------------------------ Profiling--type ProfileLabel = Text-type AllocCount = Int64-type MemoHitCount = Int64--data Profile = Profile- { profileRound :: {-# UNPACK #-} !Round- -- ^ Keep track of what the current fetch round is.- , profile :: HashMap ProfileLabel ProfileData- -- ^ Data on individual labels.- , profileCache :: DataCache (Constant Round)- -- ^ Keep track of the round requests first appear in.- }--emptyProfile :: Profile-emptyProfile = Profile 1 HashMap.empty emptyDataCache--data ProfileData = ProfileData- { profileAllocs :: {-# UNPACK #-} !AllocCount- -- ^ allocations made by this label- , profileDeps :: HashSet ProfileLabel- -- ^ labels that this label depends on- , profileFetches :: Map Round (HashMap Text Int)- -- ^ map from round to {datasource name => fetch count}- , profileMemoHits :: {-# UNPACK #-} !MemoHitCount- -- ^ number of hits to memoized computation at this label- }- deriving Show--emptyProfileData :: ProfileData-emptyProfileData = ProfileData 0 HashSet.empty Map.empty 0---- ------------------------------------------------------------------------------ DataCache---- | The 'DataCache' maps things of type @f a@ to @'ResultVar' a@, for--- any @f@ and @a@ provided @f a@ is an instance of 'Typeable'. In--- practice @f a@ will be a request type parameterised by its result.------ See the definition of 'ResultVar' for more details.--newtype DataCache res = DataCache (HashMap TypeRep (SubCache res))---- | The implementation is a two-level map: the outer level maps the--- types of requests to 'SubCache', which maps actual requests to their--- results. So each 'SubCache' contains requests of the same type.--- This works well because we only have to store the dictionaries for--- 'Hashable' and 'Eq' once per request type.-data SubCache res =- forall req a . (Hashable (req a), Eq (req a), Typeable (req a)) =>- SubCache (req a -> String) (a -> String) ! (HashMap (req a) (res a))- -- NB. the inner HashMap is strict, to avoid building up- -- a chain of thunks during repeated insertions.---- | A new, empty 'DataCache'.-emptyDataCache :: DataCache res-emptyDataCache = DataCache HashMap.empty---- ------------------------------------------------------------------------------ DataSource class---- | The class of data sources, parameterised over the request type for--- that data source. Every data source must implement this class.------ A data source keeps track of its state by creating an instance of--- 'StateKey' to map the request type to its state. In this case, the--- type of the state should probably be a reference type of some kind,--- such as 'IORef'.------ For a complete example data source, see--- <https://github.com/facebook/Haxl/tree/master/example Examples>.----class (DataSourceName req, StateKey req, ShowP req) => DataSource u req where-- -- | Issues a list of fetches to this 'DataSource'. The 'BlockedFetch'- -- objects contain both the request and the 'ResultVar's into which to put- -- the results.- fetch- :: State req- -- ^ Current state.- -> Flags- -- ^ Tracing flags.- -> u- -- ^ User environment.- -> [BlockedFetch req]- -- ^ Requests to fetch.- -> PerformFetch- -- ^ Fetch the data; see 'PerformFetch'.--class DataSourceName req where- -- | The name of this 'DataSource', used in tracing and stats. Must- -- take a dummy request.- dataSourceName :: req a -> Text---- The 'ShowP' class is a workaround for the fact that we can't write--- @'Show' (req a)@ as a superclass of 'DataSource', without also--- parameterizing 'DataSource' over @a@, which is a pain (I tried--- it). 'ShowP' seems fairly benign, though.---- | A convenience only: package up 'Eq', 'Hashable', 'Typeable', and 'Show'--- for requests into a single constraint.-type Request req a =- ( Eq (req a)- , Hashable (req a)- , Typeable (req a)- , Show (req a)- , Show a- )---- | A data source can fetch data in one of two ways.------ * Synchronously ('SyncFetch'): the fetching operation is an--- @'IO' ()@ that fetches all the data and then returns.------ * Asynchronously ('AsyncFetch'): we can do something else while the--- data is being fetched. The fetching operation takes an @'IO' ()@ as--- an argument, which is the operation to perform while the data is--- being fetched.------ See 'syncFetch' and 'asyncFetch' for example usage.----data PerformFetch- = SyncFetch (IO ())- | AsyncFetch (IO () -> IO ())---- Why does AsyncFetch contain a `IO () -> IO ()` rather than the--- alternative approach of returning the `IO` action to retrieve the--- results, which might seem better: `IO (IO ())`? The point is that--- this allows the data source to acquire resources for the purpose of--- this fetching round using the standard `bracket` pattern, so it can--- ensure that the resources acquired are properly released even if--- other data sources fail.---- | A 'BlockedFetch' is a pair of------ * The request to fetch (with result type @a@)------ * A 'ResultVar' to store either the result or an error------ We often want to collect together multiple requests, but they return--- different types, and the type system wouldn't let us put them--- together in a list because all the elements of the list must have the--- same type. So we wrap up these types inside the 'BlockedFetch' type,--- so that they all look the same and we can put them in a list.------ When we unpack the 'BlockedFetch' and get the request and the 'ResultVar'--- out, the type system knows that the result type of the request--- matches the type parameter of the 'ResultVar', so it will let us take the--- result of the request and store it in the 'ResultVar'.----data BlockedFetch r = forall a. BlockedFetch (r a) (ResultVar a)---- | Function for easily setting a fetch to a particular exception-setError :: (Exception e) => (forall a. r a -> e) -> BlockedFetch r -> IO ()-setError e (BlockedFetch req m) = putFailure m (e req)--except :: (Exception e) => e -> Either SomeException a-except = Left . toException---- | A sink for the result of a data fetch in 'BlockedFetch'-newtype ResultVar a = ResultVar (MVar (Either SomeException a))---- Why do we need an 'MVar' here? The reason is that the--- cache serves two purposes:------ 1. To cache the results of requests that were submitted in a previous round.------ 2. To remember requests that have been encountered in the current round but--- are not yet submitted, so that if we see the request again we can make--- sure that we only submit it once.------ Storing the result as an 'MVar' gives two benefits:------ * We can tell the difference between (1) and (2) by testing whether the--- 'MVar' is empty. See 'Haxl.Fetch.cached'.------ * In the case of (2), we don't have to update the cache again after the--- current round, and after the round we can read the result of each request--- from its 'MVar'. All instances of identical requests will share the same--- 'MVar' to obtain the result.--newResult :: a -> IO (ResultVar a)-newResult x = ResultVar <$> newMVar (Right x)--newEmptyResult :: IO (ResultVar a)-newEmptyResult = ResultVar <$> newEmptyMVar--putFailure :: (Exception e) => ResultVar a -> e -> IO ()-putFailure r = putResult r . except--putSuccess :: ResultVar a -> a -> IO ()-putSuccess r = putResult r . Right--putResult :: ResultVar a -> Either SomeException a -> IO ()-putResult (ResultVar var) = putMVar var--takeResult :: ResultVar a -> IO (Either SomeException a)-takeResult (ResultVar var) = takeMVar var--tryReadResult :: ResultVar a -> IO (Maybe (Either SomeException a))-tryReadResult (ResultVar var) = tryReadMVar var--tryTakeResult :: ResultVar a -> IO (Maybe (Either SomeException a))-tryTakeResult (ResultVar var) = tryTakeMVar var---- Fetch templates--stubFetch- :: (Exception e) => (forall a. r a -> e)- -> State r -> Flags -> u -> [BlockedFetch r] -> PerformFetch-stubFetch e _state _flags _si bfs = SyncFetch $ mapM_ (setError e) bfs---- | Common implementation templates for 'fetch' of 'DataSource'.------ Example usage:------ > fetch = syncFetch MyDS.withService MyDS.retrieve--- > $ \service request -> case request of--- > This x -> MyDS.fetchThis service x--- > That y -> MyDS.fetchThat service y----asyncFetchWithDispatch- :: ((service -> IO ()) -> IO ())- -- ^ Wrapper to perform an action in the context of a service.-- -> (service -> IO ())- -- ^ Dispatch all the pending requests-- -> (service -> IO ())- -- ^ Wait for the results-- -> (forall a. service -> request a -> IO (IO (Either SomeException a)))- -- ^ Enqueue an individual request to the service.-- -> State request- -- ^ Currently unused.-- -> Flags- -- ^ Currently unused.-- -> u- -- ^ Currently unused.-- -> [BlockedFetch request]- -- ^ Requests to submit.-- -> PerformFetch--asyncFetch, syncFetch- :: ((service -> IO ()) -> IO ())- -- ^ Wrapper to perform an action in the context of a service.-- -> (service -> IO ())- -- ^ Dispatch all the pending requests and wait for the results-- -> (forall a. service -> request a -> IO (IO (Either SomeException a)))- -- ^ Submits an individual request to the service.-- -> State request- -- ^ Currently unused.-- -> Flags- -- ^ Currently unused.-- -> u- -- ^ Currently unused.-- -> [BlockedFetch request]- -- ^ Requests to submit.-- -> PerformFetch--asyncFetchWithDispatch- withService dispatch wait enqueue _state _flags _si requests =- AsyncFetch $ \inner -> withService $ \service -> do- getResults <- mapM (submitFetch service enqueue) requests- dispatch service- inner- wait service- sequence_ getResults--asyncFetch withService wait enqueue _state _flags _si requests =- AsyncFetch $ \inner -> withService $ \service -> do- getResults <- mapM (submitFetch service enqueue) requests- inner- wait service- sequence_ getResults--syncFetch withService dispatch enqueue _state _flags _si requests =- SyncFetch . withService $ \service -> do- getResults <- mapM (submitFetch service enqueue) requests- dispatch service- sequence_ getResults---{- |-A version of 'asyncFetch' (actually 'asyncFetchWithDispatch') that-handles exceptions correctly. You should use this instead of-'asyncFetch' or 'asyncFetchWithDispatch'. The danger with-'asyncFetch' is that if an exception is thrown by @withService@, the-@inner@ action won't be executed, and we'll drop some data-fetches in-the same round.--'asyncFetchAcquireRelease' behaves like the following:--> asyncFetchAcquireRelease acquire release dispatch wait enqueue =-> AsyncFetch $ \inner ->-> bracket acquire release $ \service -> do-> getResults <- mapM (submitFetch service enqueue) requests-> dispatch service-> inner-> wait service-> sequence_ getResults--except that @inner@ is run even if @acquire@, @enqueue@, or @dispatch@ throws,-/unless/ an async exception is received.--}--asyncFetchAcquireRelease- :: IO service- -- ^ Resource acquisition for this datasource-- -> (service -> IO ())- -- ^ Resource release-- -> (service -> IO ())- -- ^ Dispatch all the pending requests and wait for the results-- -> (service -> IO ())- -- ^ Wait for the results-- -> (forall a. service -> request a -> IO (IO (Either SomeException a)))- -- ^ Submits an individual request to the service.-- -> State request- -- ^ Currently unused.-- -> Flags- -- ^ Currently unused.-- -> u- -- ^ Currently unused.-- -> [BlockedFetch request]- -- ^ Requests to submit.-- -> PerformFetch--asyncFetchAcquireRelease- acquire release dispatch wait enqueue _state _flags _si requests =- AsyncFetch $ \inner -> mask $ \restore -> do- r1 <- tryWithRethrow acquire- case r1 of- Left err -> do restore inner; throwIO (err :: SomeException)- Right service -> do- flip finally (release service) $ restore $ do- r2 <- tryWithRethrow $ do- getResults <- mapM (submitFetch service enqueue) requests- dispatch service- return getResults- inner -- we assume this cannot throw, ensured by performFetches- case r2 of- Left err -> throwIO (err :: SomeException)- Right getResults -> do wait service; sequence_ getResults---- | Used by 'asyncFetch' and 'syncFetch' to retrieve the results of--- requests to a service.-submitFetch- :: service- -> (forall a. service -> request a -> IO (IO (Either SomeException a)))- -> BlockedFetch request- -> IO (IO ())-submitFetch service fetch (BlockedFetch request result)- = (putResult result =<<) <$> fetch service request
Haxl/Core/Util.hs view
@@ -2,23 +2,15 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.--{-# LANGUAGE CPP #-}+-- found in the LICENSE file. +-- | Internal utilities only.+-- module Haxl.Core.Util ( compose , textShow- , tryReadMVar ) where -#if __GLASGOW_HASKELL__ >= 708-import Control.Concurrent (tryReadMVar)-#else-import Control.Concurrent-#endif- import Data.Text (Text) import qualified Data.Text as Text@@ -26,15 +18,6 @@ -- | Composes a list of endofunctions. compose :: [a -> a] -> a -> a compose = foldr (.) id--#if __GLASGOW_HASKELL__ < 708-tryReadMVar :: MVar a -> IO (Maybe a)-tryReadMVar m = do- mb <- tryTakeMVar m- case mb of- Nothing -> return Nothing- Just a -> putMVar m a >> return (Just a)-#endif textShow :: (Show a) => a -> Text textShow = Text.pack . show
+ Haxl/DataSource/ConcurrentIO.hs view
@@ -0,0 +1,83 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- A generic Haxl datasource for performing arbitrary IO concurrently.+-- Every IO operation will be performed in a separate thread.+-- You can use this with any kind of IO, but each different operation+-- requires an instance of the 'ConcurrentIO' class.+--+-- For example, to make a concurrent sleep operation:+--+-- > sleep :: Int -> GenHaxl u Int+-- > sleep n = dataFetch (Sleep n)+-- >+-- > data Sleep+-- > instance ConcurrentIO Sleep where+-- > data ConcurrentIOReq Sleep a where+-- > Sleep :: Int -> ConcurrentIOReq Sleep Int+-- >+-- > performIO (Sleep n) = threadDelay (n*1000) >> return n+-- >+-- > deriving instance Eq (ConcurrentIOReq Sleep a)+-- > deriving instance Show (ConcurrentIOReq Sleep a)+-- >+-- > instance ShowP (ConcurrentIOReq Sleep) where showp = show+-- >+-- > instance Hashable (ConcurrentIOReq Sleep a) where+-- > hashWithSalt s (Sleep n) = hashWithSalt s n+--+-- Note that you can have any number of constructors in your+-- ConcurrentIOReq GADT, so most of the boilerplate only needs to be+-- written once.++module Haxl.DataSource.ConcurrentIO+ ( mkConcurrentIOState+ , ConcurrentIO(..)+ ) where++import Control.Concurrent+import Control.Exception as Exception+import Control.Monad+import qualified Data.Text as Text+import Data.Typeable++import Haxl.Core++class ConcurrentIO tag where+ data ConcurrentIOReq tag a+ performIO :: ConcurrentIOReq tag a -> IO a++deriving instance Typeable ConcurrentIOReq -- not needed by GHC 7.10 and later++instance (Typeable tag) => StateKey (ConcurrentIOReq tag) where+ data State (ConcurrentIOReq tag) = ConcurrentIOState+ getStateType _ = typeRep (Proxy :: Proxy ConcurrentIOReq)++mkConcurrentIOState :: IO (State (ConcurrentIOReq ()))+mkConcurrentIOState = return ConcurrentIOState++instance Typeable tag => DataSourceName (ConcurrentIOReq tag) where+ dataSourceName _ =+ Text.pack (show (typeRepTyCon (typeRep (Proxy :: Proxy tag))))++instance+ (Typeable tag, ShowP (ConcurrentIOReq tag), ConcurrentIO tag)+ => DataSource u (ConcurrentIOReq tag)+ where+ fetch _state _flags _u = BackgroundFetch $ \bfs -> do+ forM_ bfs $ \(BlockedFetch req rv) ->+ mask $ \unmask ->+ forkFinally (unmask (performIO req)) (putResultFromChildThread rv)
Haxl/Prelude.hs view
@@ -2,8 +2,7 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-}@@ -66,10 +65,12 @@ ) where -import Haxl.Core.Types+import Haxl.Core.DataSource import Haxl.Core.Exception import Haxl.Core.Memo import Haxl.Core.Monad+import Haxl.Core.Fetch+import Haxl.Core.Parallel import Control.Applicative import Control.Monad (foldM, join, void)
− PATENTS
@@ -1,33 +0,0 @@-Additional Grant of Patent Rights Version 2--"Software" means the Haxl software distributed by Facebook, Inc.--Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software-("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable-(subject to the termination provision below) license under any Necessary-Claims, to make, have made, use, sell, offer to sell, import, and otherwise-transfer the Software. For avoidance of doubt, no license is granted under-Facebook’s rights in any patent claims that are infringed by (i) modifications-to the Software made by you or any third party or (ii) the Software in-combination with any software or other technology.--The license granted hereunder will terminate, automatically and without notice,-if you (or any of your subsidiaries, corporate affiliates or agents) initiate-directly or indirectly, or take a direct financial interest in, any Patent-Assertion: (i) against Facebook or any of its subsidiaries or corporate-affiliates, (ii) against any party if such Patent Assertion arises in whole or-in part from any software, technology, product or service of Facebook or any of-its subsidiaries or corporate affiliates, or (iii) against any party relating-to the Software. Notwithstanding the foregoing, if Facebook or any of its-subsidiaries or corporate affiliates files a lawsuit alleging patent-infringement against you in the first instance, and you respond by filing a-patent infringement counterclaim in that lawsuit against that party that is-unrelated to the Software, the license granted hereunder will not terminate-under section (i) of this paragraph due to such counterclaim.--A "Necessary Claim" is a claim of a patent owned by Facebook that is-necessarily infringed by the Software standing alone.--A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,-or contributory infringement or inducement to infringe any patent, including a-cross-claim or counterclaim.
changelog.md view
@@ -1,3 +1,26 @@+# Changes in version 2.0.0.0++ * Completely rewritten internals to support arbitrarily overlapping+ I/O and computation. Haxl no longer runs batches of I/O in+ "rounds", waiting for all the I/O to complete before resuming the+ computation. In Haxl 2, we can spawn I/O that returns results in+ the background and computation fragments are resumed when the+ values they depend on are available. See+ `tests/FullyAsyncTest.hs` for an example.++ * A new `PerformFetch` constructor supports the new concurrency+ features: `BackgroundFetch`. The data source is expected to call+ `putResult` in the background on each `BlockedFetch` when its+ result is ready.++ * There is a generic `DataSource` implementation in+ `Haxl.DataSource.ConcurrentIO` for performing each I/O operation+ in a separate thread.++ * Lots of cleanup and refactoring of the APIs.++ * License changed from BSD+PATENTS to plain BSD3.+ # Changes in version 0.5.1.0 * 'pAnd' and 'pOr' were added
haxl.cabal view
@@ -1,11 +1,11 @@ name: haxl-version: 0.5.1.0+version: 2.0.0.0 synopsis: A Haskell library for efficient, concurrent, and concise data access. homepage: https://github.com/facebook/Haxl bug-reports: https://github.com/facebook/Haxl/issues-license: OtherLicense-license-files: LICENSE,PATENTS+license: BSD3+license-files: LICENSE author: Facebook, Inc. maintainer: The Haxl Team <haxl-team@fb.com> copyright: Copyright (c) 2014-present, Facebook, Inc.@@ -17,7 +17,8 @@ GHC==7.8.4, GHC==7.10.3, GHC==8.0.2,- GHC==8.2.1+ GHC==8.2.2,+ GHC==8.4.1 description: Haxl is a library and EDSL for efficient scheduling of concurrent data@@ -36,21 +37,19 @@ extra-source-files: readme.md- PATENTS tests/LoadCache.txt changelog.md library build-depends:- HUnit >= 1.2 && < 1.7,- aeson >= 0.6 && < 1.3,+ aeson >= 0.6 && < 1.4, base == 4.*, binary >= 0.7 && < 0.10, bytestring >= 0.9 && < 0.11, containers == 0.5.*, deepseq,- exceptions >=0.8 && <0.9,+ exceptions >=0.8 && <0.11, filepath >= 1.3 && < 1.5, ghc-prim, hashable == 1.2.*,@@ -58,21 +57,33 @@ -- text 1.2.1.0 required for instance Binary Text text >= 1.2.1.0 && < 1.3, time >= 1.4 && < 1.9,+ stm == 2.4.*, transformers, unordered-containers == 0.2.*, vector >= 0.10 && <0.13 + if !impl(ghc >= 8.0)+ build-depends:+ semigroups >= 0.18.3 && <0.19+ exposed-modules: Haxl.Core, Haxl.Core.DataCache,+ Haxl.Core.DataSource, Haxl.Core.Exception,+ Haxl.Core.Flags, Haxl.Core.Memo, Haxl.Core.Monad,+ Haxl.Core.Fetch,+ Haxl.Core.Parallel,+ Haxl.Core.Profile,+ Haxl.Core.Run, Haxl.Core.RequestStore,- Haxl.Core.StateStore, Haxl.Core.ShowP,- Haxl.Core.Types,+ Haxl.Core.StateStore,+ Haxl.Core.Stats, Haxl.Prelude+ Haxl.DataSource.ConcurrentIO other-modules: Haxl.Core.Util@@ -80,14 +91,19 @@ default-language: Haskell2010 ghc-options:+ -O2 -fprof-auto -Wall + if impl(ghc >= 8.0)+ ghc-options: -Wno-name-shadowing+ else+ ghc-options: -fno-warn-name-shadowing test-suite test build-depends:- HUnit,- aeson,+ aeson >= 0.6 && < 1.3,+ HUnit >= 1.2 && < 1.7, base == 4.*, binary, bytestring,@@ -113,30 +129,40 @@ main-is: TestMain.hs + if impl(ghc >= 8.0)+ other-modules:+ AdoTests+ other-modules:- AdoTests AllTests BadDataSource BatchTests- Bench CoreTests DataCacheTest ExampleDataSource+ FullyAsyncTest LoadCache MemoizationTests MockTAO ProfileTests+ SleepDataSource TestBadDataSource TestExampleDataSource TestTypes TestUtils+ WorkDataSource type: exitcode-stdio-1.0 default-language: Haskell2010 +flag bench+ default: False+ executable monadbench+ if !flag(bench)+ buildable: False default-language: Haskell2010 hs-source-dirs:@@ -152,3 +178,20 @@ ExampleDataSource ghc-options: -O2 -main-is MonadBench -rtsopts++executable cachebench+ if !flag(bench)+ buildable: False+ default-language:+ Haskell2010+ hs-source-dirs:+ tests+ build-depends:+ base,+ haxl,+ hashable,+ time+ main-is:+ Bench.hs+ ghc-options:+ -O2 -main-is Bench -rtsopts
readme.md view
@@ -22,6 +22,11 @@ To use Haxl in your own application, you will likely need to build one or more *data sources*: the thin layer between Haxl and the data that you want to fetch, be it a database, a web API, a cloud service, or whatever.++There is a generic datasource in "Haxl.DataSource.ConcurrentIO" that+can be used for performing arbitrary IO operations concurrently, given+a bit of boilerplate to define the IO operations you want to perform.+ The `haxl-facebook` package shows how we might build a Haxl data source based on the existing `fb` package for talking to the Facebook Graph API.@@ -35,6 +40,10 @@ * [An example Facebook data source](example/facebook/readme.md) walks through building an example data source that queries the Facebook Graph API concurrently.++ * [Fun with Haxl (part 1)](https://simonmar.github.io/posts/2015-10-20-Fun-With-Haxl-1.html)+ Walks through using Haxl from scratch for a simple SQLite-backed+ blog engine. * [The N+1 Selects Problem](example/sql/readme.md) explains how Haxl can address a common performance problem with SQL queries by
tests/AdoTests.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE RebindableSyntax, OverloadedStrings, ApplicativeDo #-} module AdoTests (tests) where @@ -42,8 +48,8 @@ b' <- friendsOf =<< if null b then id4 else id3 return (length (a' ++ b')) -tests = TestList- [ TestLabel "ado1" $ TestCase ado1- , TestLabel "ado2" $ TestCase ado2- , TestLabel "ado3" $ TestCase ado3+tests future = TestList+ [ TestLabel "ado1" $ TestCase (ado1 future)+ , TestLabel "ado2" $ TestCase (ado2 future)+ , TestLabel "ado3" $ TestCase (ado3 future) ]
tests/AllTests.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE CPP, OverloadedStrings #-} module AllTests (allTests) where @@ -13,21 +19,24 @@ #endif import MemoizationTests import TestBadDataSource+import FullyAsyncTest import Test.HUnit allTests :: Test allTests = TestList [ TestLabel "ExampleDataSource" TestExampleDataSource.tests- , TestLabel "BatchTests" BatchTests.tests+ , TestLabel "BatchTests-future" $ BatchTests.tests True+ , TestLabel "BatchTests-sync" $ BatchTests.tests False , TestLabel "CoreTests" CoreTests.tests , TestLabel "DataCacheTests" DataCacheTest.tests #if __GLASGOW_HASKELL__ >= 801- , TestLabel "AdoTests" AdoTests.tests+ , TestLabel "AdoTests" $ AdoTests.tests False #endif #if __GLASGOW_HASKELL__ >= 710 , TestLabel "ProfileTests" ProfileTests.tests #endif , TestLabel "MemoizationTests" MemoizationTests.tests , TestLabel "BadDataSourceTests" TestBadDataSource.tests+ , TestLabel "FullyAsyncTest" FullyAsyncTest.tests ]
tests/BadDataSource.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}
tests/BatchTests.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE RebindableSyntax, OverloadedStrings #-} module BatchTests (tests) where @@ -127,20 +133,20 @@ -- -- Basic sanity check on data-cache re-use ---cacheReuse = do- env <- makeTestEnv+cacheReuse future = do+ env <- makeTestEnv future expectRoundsWithEnv 2 12 batching7_ env -- make a new env- tao <- MockTAO.initGlobalState+ tao <- MockTAO.initGlobalState future let st = stateSet tao stateEmpty env2 <- initEnvWithData st testinput (caches env) -- ensure no more data fetching rounds needed expectRoundsWithEnv 0 12 batching7_ env2 -noCaching = do- env <- makeTestEnv+noCaching future = do+ env <- makeTestEnv future let env' = env{ flags = (flags env){caching = 0} } result <- runHaxl env' caching3_ assertEqual "result" result 18@@ -155,8 +161,8 @@ (withDefault [] (friendsOf 101)) (withDefault [] (friendsOf 2)) -deterministicExceptions = do- env <- makeTestEnv+deterministicExceptions future = do+ env <- makeTestEnv future let haxl = sequence [ do _ <- friendsOf =<< id1; throw (NotFound "xxx") , throw (NotFound "yyy")@@ -176,8 +182,8 @@ Left (NotFound "xxx") -> True _ -> False -pOrTests = do- env <- makeTestEnv+pOrTests future = do+ env <- makeTestEnv future -- Test semantics r <- runHaxl env $ do@@ -188,7 +194,7 @@ return (not a && b && c && d) assertBool "pOr0" r - -- pOr is left-biased with respsect to exceptions:+ -- pOr is left-biased with respect to exceptions: r <- runHaxl env $ try $ return True `pOr` throw (NotFound "foo") assertBool "pOr1" $ case (r :: Either NotFound Bool) of@@ -224,8 +230,8 @@ Left (NotFound _) -> True _ -> False -pAndTests = do- env <- makeTestEnv+pAndTests future = do+ env <- makeTestEnv future -- Test semantics r <- runHaxl env $ do@@ -236,7 +242,7 @@ return (not a && not b && not c && d) assertBool "pAnd0" r - -- pAnd is left-biased with respsect to exceptions:+ -- pAnd is left-biased with respect to exceptions: r <- runHaxl env $ try $ return False `pAnd` throw (NotFound "foo") assertBool "pAnd1" $ case (r :: Either NotFound Bool) of@@ -273,24 +279,26 @@ Left (NotFound _) -> True _ -> False -tests = TestList- [ TestLabel "batching1" $ TestCase batching1- , TestLabel "batching2" $ TestCase batching2- , TestLabel "batching3" $ TestCase batching3- , TestLabel "batching4" $ TestCase batching4- , TestLabel "batching5" $ TestCase batching5- , TestLabel "batching6" $ TestCase batching6- , TestLabel "batching7" $ TestCase batching7- , TestLabel "batching8" $ TestCase batching8- , TestLabel "batching9" $ TestCase batching9- , TestLabel "caching1" $ TestCase caching1- , TestLabel "caching2" $ TestCase caching2- , TestLabel "caching3" $ TestCase caching3- , TestLabel "CacheReuse" $ TestCase cacheReuse- , TestLabel "NoCaching" $ TestCase noCaching- , TestLabel "exceptionTest1" $ TestCase exceptionTest1- , TestLabel "exceptionTest2" $ TestCase exceptionTest2- , TestLabel "deterministicExceptions" $ TestCase deterministicExceptions- , TestLabel "pOrTest" $ TestCase pOrTests- , TestLabel "pAndTest" $ TestCase pAndTests+tests :: Bool -> Test+tests future = TestList+ [ TestLabel "batching1" $ TestCase (batching1 future)+ , TestLabel "batching2" $ TestCase (batching2 future)+ , TestLabel "batching3" $ TestCase (batching3 future)+ , TestLabel "batching4" $ TestCase (batching4 future)+ , TestLabel "batching5" $ TestCase (batching5 future)+ , TestLabel "batching6" $ TestCase (batching6 future)+ , TestLabel "batching7" $ TestCase (batching7 future)+ , TestLabel "batching8" $ TestCase (batching8 future)+ , TestLabel "batching9" $ TestCase (batching9 future)+ , TestLabel "caching1" $ TestCase (caching1 future)+ , TestLabel "caching2" $ TestCase (caching2 future)+ , TestLabel "caching3" $ TestCase (caching3 future)+ , TestLabel "CacheReuse" $ TestCase (cacheReuse future)+ , TestLabel "NoCaching" $ TestCase (noCaching future)+ , TestLabel "exceptionTest1" $ TestCase (exceptionTest1 future)+ , TestLabel "exceptionTest2" $ TestCase (exceptionTest2 future)+ , TestLabel "deterministicExceptions" $+ TestCase (deterministicExceptions future)+ , TestLabel "pOrTest" $ TestCase (pOrTests future)+ , TestLabel "pAndTest" $ TestCase (pAndTests future) ]
tests/Bench.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE RankNTypes, GADTs, BangPatterns, DeriveDataTypeable, StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-unused-do-bind -fno-warn-type-defaults #-}@@ -5,11 +11,11 @@ module Bench where import Haxl.Core.DataCache as DataCache-import Haxl.Core.Types import Prelude hiding (mapM) import Data.Hashable+import Data.IORef import Data.Time.Clock import Data.Traversable import Data.Typeable@@ -36,12 +42,12 @@ let f 0 !cache = return cache f !n !cache = do- m <- newResult 0+ m <- newIORef 0 f (n-1) (DataCache.insert (ReqInt n) m cache) -- cache <- f n emptyDataCache let m = DataCache.lookup (ReqInt (n `div` 2)) cache- print =<< mapM takeResult m+ print =<< mapM readIORef m t1 <- getCurrentTime printf "insert: %.2fs\n" (realToFrac (t1 `diffUTCTime` t0) :: Double)
tests/CoreTests.hs view
@@ -1,6 +1,13 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE RebindableSyntax #-}+ module CoreTests where import Haxl.Prelude@@ -12,17 +19,29 @@ import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as BS+import Data.List import Control.Exception (Exception(..)) +import ExampleDataSource++testEnv = do+ -- To use a data source, we need to initialize its state:+ exstate <- ExampleDataSource.initGlobalState++ -- And create a StateStore object containing the states we need:+ let st = stateSet exstate stateEmpty++ -- Create the Env:+ initEnv st ()+ useless :: String -> GenHaxl u Bool useless _ = throw (NotFound "ha ha") -en = error "no env"- exceptions :: Assertion exceptions = do+ en <- emptyEnv () a <- runHaxl en $ try (useless "input") assertBool "NotFound -> HaxlException" $ isLeft (a :: Either HaxlException Bool)@@ -72,6 +91,38 @@ return True) `catch` \InternalError{} -> return False assertBool "catchIf2" (not e)++ -- test tryToHaxlException+ e <- runHaxl en $ tryToHaxlException $ head []+ assertBool "tryToHaxlException1" $+ case e of+ Left ex | Just NonHaxlException{} <- fromException (toException ex)+ -> True+ _ -> False++ env <- testEnv+ e <- runHaxl env $ tryToHaxlException $ do+ xs <- listWombats 3+ return $! length xs `quot` 0+ print e+ assertBool "tryToHaxlException1" $+ case e of+ Left ex | Just NonHaxlException{} <- fromException (toException ex)+ -> True+ _ -> False++ env <- testEnv+ e <- runHaxl env $ mapM tryToHaxlException+ [ do xs <- listWombats 3; return $! length xs `quot` 0+ , head []+ ]+ print e+ assertBool "tryToHaxlException2" $+ case e of+ [Left ex1, Left ex2]+ | "divide" `isInfixOf` show ex1+ , "head" `isInfixOf` show ex2 -> True+ _ -> False where isLeft Left{} = True isLeft _ = False@@ -80,7 +131,9 @@ -- This is mostly a compile test, to make sure all the plumbing -- makes the compiler happy. base :: (Exception a) => a -> IO HaxlException-base e = runHaxl en $ throw e `catch` \x -> return x+base e = do+ en <- emptyEnv ()+ runHaxl en $ throw e `catch` \x -> return x printing :: Assertion printing = do@@ -98,7 +151,35 @@ BS.putStrLn $ encode c +withEnvTest :: Test+withEnvTest = TestLabel "withEnvTest" $ TestCase $ do+ exstate <- ExampleDataSource.initGlobalState+ e <- initEnv (stateSet exstate stateEmpty) False+ b <- runHaxl e $ withEnv e { userEnv = True } $ env userEnv+ assertBool "withEnv1" b+ e <- initEnv (stateSet exstate stateEmpty) False+ b <- runHaxl e $ withEnv e { userEnv = True } $ do+ _ <- countAardvarks "aaa"+ env userEnv+ assertBool "withEnv2" b+ e <- initEnv (stateSet exstate stateEmpty) False+ b <- runHaxl e $ withEnv e { userEnv = True } $ do+ memo ("xxx" :: Text) $ do+ _ <- countAardvarks "aaa"+ env userEnv+ assertBool "withEnv3" b+ e <- initEnv (stateSet exstate stateEmpty) False+ b <- runHaxl e $+ withEnv e { userEnv = True } $ do+ memo ("yyy" :: Text) $ do+ _ <- countAardvarks "aaa"+ _ <- countAardvarks "bbb"+ env userEnv+ assertBool "withEnv4" b++ tests = TestList [ TestLabel "exceptions" $ TestCase exceptions,- TestLabel "print_stuff" $ TestCase printing+ TestLabel "print_stuff" $ TestCase printing,+ TestLabel "withEnv" $ withEnvTest ]
tests/DataCacheTest.hs view
@@ -1,15 +1,26 @@-{-# LANGUAGE StandaloneDeriving, GADTs, DeriveDataTypeable #-}-module DataCacheTest (tests) where+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file. +{-# LANGUAGE CPP, StandaloneDeriving, GADTs, DeriveDataTypeable #-}+module DataCacheTest (tests, newResult, takeResult) where+ import Haxl.Core.DataCache as DataCache+import Haxl.Core.Monad import Haxl.Core +#if __GLASGOW_HASKELL__ < 710+import Control.Applicative+#endif import Control.Exception import Data.Hashable import Data.Traversable import Data.Typeable import Prelude hiding (mapM) import Test.HUnit+import Data.IORef data TestReq a where Req :: Int -> TestReq a -- polymorphic result@@ -21,7 +32,17 @@ instance Hashable (TestReq a) where hashWithSalt salt (Req i) = hashWithSalt salt i +newResult :: a -> IO (IVar u a)+newResult a = IVar <$> newIORef (IVarFull (Ok a)) +takeResult :: IVar u a -> IO (ResultVal a)+takeResult (IVar ref) = do+ e <- readIORef ref+ case e of+ IVarFull a -> return a+ _ -> error "takeResult"++ dcSoundnessTest :: Test dcSoundnessTest = TestLabel "DataCache soundness" $ TestCase $ do m1 <- newResult 1@@ -35,25 +56,25 @@ -- with a result of type String, we should get Nothing, not a crash. r <- mapM takeResult $ DataCache.lookup (Req 1) cache assertBool "dcSoundness1" $- case r :: Maybe (Either SomeException String) of+ case r :: Maybe (ResultVal String) of Nothing -> True _something_else -> False r <- mapM takeResult $ DataCache.lookup (Req 1) cache assertBool "dcSoundness2" $- case r :: Maybe (Either SomeException Int) of- Just (Right 1) -> True+ case r :: Maybe (ResultVal Int) of+ Just (Ok 1) -> True _something_else -> False r <- mapM takeResult $ DataCache.lookup (Req 2) cache assertBool "dcSoundness3" $- case r :: Maybe (Either SomeException String) of- Just (Right "hello") -> True+ case r :: Maybe (ResultVal String) of+ Just (Ok "hello") -> True _something_else -> False r <- mapM takeResult $ DataCache.lookup (Req 2) cache assertBool "dcSoundness4" $- case r :: Maybe (Either SomeException Int) of+ case r :: Maybe (ResultVal Int) of Nothing -> True _something_else -> False
tests/ExampleDataSource.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -117,10 +123,9 @@ exampleFetch :: State ExampleReq -- current state -> Flags -- tracing verbosity, etc. -> u -- user environment- -> [BlockedFetch ExampleReq] -- requests to fetch- -> PerformFetch -- tells the framework how to fetch+ -> PerformFetch ExampleReq -- tells the framework how to fetch -exampleFetch _state _flags _user bfs = SyncFetch $ mapM_ fetch1 bfs+exampleFetch _state _flags _user = SyncFetch $ mapM_ fetch1 -- There are two ways a data source can fetch data: synchronously or -- asynchronously. See the type 'PerformFetch' in "Haxl.Core.Types" for@@ -150,8 +155,8 @@ -- Normally a data source will provide some convenient wrappers for -- its requests: -countAardvarks :: String -> GenHaxl () Int+countAardvarks :: String -> GenHaxl u Int countAardvarks str = dataFetch (CountAardvarks str) -listWombats :: Id -> GenHaxl () [Id]+listWombats :: Id -> GenHaxl u [Id] listWombats i = dataFetch (ListWombats i)
+ tests/FullyAsyncTest.hs view
@@ -0,0 +1,66 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++module FullyAsyncTest where++import Haxl.Prelude as Haxl+import Prelude()++import SleepDataSource+import Haxl.DataSource.ConcurrentIO++import Haxl.Core+import Test.HUnit+import Data.IORef+import Haxl.Core.Monad (unsafeLiftIO)++tests :: Test+tests = sleepTest++testEnv = do+ st <- mkConcurrentIOState+ env <- initEnv (stateSet st stateEmpty) ()+ return env { flags = (flags env) { report = 2 } }++sleepTest :: Test+sleepTest = TestCase $ do+ env <- testEnv++ ref <- newIORef ([] :: [Int])+ let tick n = unsafeLiftIO (modifyIORef ref (n:))++ -- simulate running a selection of data fetches that complete at+ -- different times, overlapping them as much as possible.+ runHaxl env $+ sequence_+ [ sequence_ [sleep 100, sleep 400] `andThen` tick 5 -- A+ , sleep 100 `andThen` tick 2 `andThen` sleep 200 `andThen` tick 4 -- B+ , sleep 50 `andThen` tick 1 `andThen` sleep 150 `andThen` tick 3 -- C+ ]++ ys <- readIORef ref+ assertEqual "FullyAsyncTest: ordering" [1,2,3,4,5] (reverse ys)++ stats <- readIORef (statsRef env)+ print stats+ assertEqual "FullyAsyncTest: stats" 5 (numFetches stats)++andThen :: GenHaxl u a -> GenHaxl u b -> GenHaxl u b+andThen a b = a >>= \_ -> b++{-+ A B C+50 | | tick 1+100 | tick 2 |+150 | | |+200 | | tick 3+250 | |+300 | tick 4+350 |+400 |+450 |+500 tick 5+-}
tests/LoadCache.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE CPP, OverloadedStrings #-} module LoadCache where
tests/MemoizationTests.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE CPP #-} module MemoizationTests (tests) where @@ -9,7 +15,7 @@ import Test.HUnit import Haxl.Core-import Haxl.Core.Monad+import Haxl.Core.Monad (unsafeLiftIO) import ExampleDataSource
tests/MockTAO.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -6,6 +12,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-} module MockTAO ( Id(..),@@ -43,16 +50,18 @@ hashWithSalt s (AssocRangeId2s a b) = hashWithSalt s (a,b) instance StateKey TAOReq where- data State TAOReq = TAOState {}+ data State TAOReq = TAOState { future :: Bool } instance DataSourceName TAOReq where dataSourceName _ = "MockTAO" instance DataSource UserEnv TAOReq where- fetch _state _flags _user bfs = SyncFetch $ mapM_ doFetch bfs+ fetch TAOState{..} _flags _user+ | future = FutureFetch $ return . mapM_ doFetch+ | otherwise = SyncFetch $ mapM_ doFetch -initGlobalState :: IO (State TAOReq)-initGlobalState = return TAOState {}+initGlobalState :: Bool -> IO (State TAOReq)+initGlobalState future = return TAOState { future=future } doFetch :: BlockedFetch TAOReq -> IO () doFetch (BlockedFetch req@(AssocRangeId2s a b) r) =
tests/MonadBench.hs view
@@ -2,16 +2,10 @@ -- All rights reserved. -- -- This source code is distributed under the terms of a BSD license,--- found in the LICENSE file. An additional grant of patent rights can--- be found in the PATENTS file.+-- found in the LICENSE file. -- | Benchmarking tool for core performance characteristics of the Haxl monad. {-# LANGUAGE CPP #-}-#if __GLASGOW_HASKELL >= 800-{-# OPTIONS_GHC -Wno-name-shadowing #-}-#else-{-# OPTIONS_GHC -fno-warn-name-shadowing #-}-#endif module MonadBench (main) where import Control.Monad@@ -25,7 +19,7 @@ import Haxl.Prelude as Haxl import Prelude() -import Haxl.Core.Monad (newMemoWith, runMemo)+import Haxl.Core.Memo (newMemoWith, runMemo) import Haxl.Core @@ -118,7 +112,8 @@ ] exitWith (ExitFailure 1) t1 <- getCurrentTime- printf "%d reqs: %.2fs\n" n (realToFrac (t1 `diffUTCTime` t0) :: Double)+ printf "%10s: %10d reqs: %.2fs\n"+ test n (realToFrac (t1 `diffUTCTime` t0) :: Double) where -- can't use >>, it is aliased to *> and we want the real bind here andThen x y = x >>= const y
tests/ProfileTests.hs view
@@ -1,25 +1,35 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+ module ProfileTests where import Haxl.Prelude-import Data.List import Haxl.Core import Haxl.Core.Monad+import Haxl.Core.Stats+import Haxl.DataSource.ConcurrentIO import Test.HUnit import Control.DeepSeq (force) import Control.Exception (evaluate)+import Data.Aeson import Data.IORef import qualified Data.HashMap.Strict as HashMap import qualified Data.HashSet as HashSet import TestUtils+import WorkDataSource mkProfilingEnv = do- env <- makeTestEnv+ env <- makeTestEnv False return env { flags = (flags env) { report = 4 } } collectsdata :: Assertion@@ -29,17 +39,20 @@ withLabel "bar" $ withLabel "foo" $ do u <- env userEnv- if length (intersect (HashMap.keys u) ["c"]) > 1- then return 5- else return (4::Int)+ -- do some non-trivial work that can't be lifted out+ case fromJSON <$> HashMap.lookup "A" u of+ Just (Success n) | sum [n .. 1000::Integer] > 0 -> return 5+ _otherwise -> return (4::Int) profData <- profile <$> readIORef (profRef e) assertEqual "has data" 3 $ HashMap.size profData assertBool "foo allocates" $ case profileAllocs <$> HashMap.lookup "foo" profData of- Just x -> x > 0+ Just x -> x > 10000 Nothing -> False- assertEqual "bar does not allocate" (Just 0) $- profileAllocs <$> HashMap.lookup "bar" profData+ assertBool "bar does not allocate (much)" $+ case profileAllocs <$> HashMap.lookup "bar" profData of+ Just n -> n < 5000 -- getAllocationCounter can be off by +/- 4K+ _otherwise -> False assertEqual "foo's parent" (Just ["bar"]) $ HashSet.toList . profileDeps <$> HashMap.lookup "foo" profData @@ -63,7 +76,24 @@ assertBool "inner label added" $ HashMap.member "inner" profData ++-- Test that we correctly attribute work done in child threads when+-- using BackgroundFetch to the caller of runHaxl. This is important+-- for correct accounting when relying on allocation limits.+threadAlloc :: Assertion+threadAlloc = do+ st <- mkConcurrentIOState+ env <- initEnv (stateSet st stateEmpty) ()+ a0 <- getAllocationCounter+ _x <- runHaxl env $ work 100000+ a1 <- getAllocationCounter+ assertBool "threadAlloc" $ (a0 - a1) > 1000000+ -- the result was 16MB on 64-bit, or around 25KB if we miss the allocs+ -- in the child thread.++ tests = TestList [ TestLabel "collectsdata" $ TestCase collectsdata , TestLabel "exceptions" $ TestCase exceptions+ , TestLabel "threads" $ TestCase threadAlloc ]
+ tests/SleepDataSource.hs view
@@ -0,0 +1,46 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE FlexibleInstances #-}++module SleepDataSource (+ sleep,+ ) where++import Haxl.Prelude+import Prelude ()++import Haxl.Core+import Haxl.DataSource.ConcurrentIO++import Control.Concurrent+import Data.Hashable+import Data.Typeable++sleep :: Int -> GenHaxl u Int+sleep n = dataFetch (Sleep n)++data Sleep deriving Typeable+instance ConcurrentIO Sleep where+ data ConcurrentIOReq Sleep a where+ Sleep :: Int -> ConcurrentIOReq Sleep Int++ performIO (Sleep n) = threadDelay (n*1000) >> return n++deriving instance Eq (ConcurrentIOReq Sleep a)+deriving instance Show (ConcurrentIOReq Sleep a)++instance ShowP (ConcurrentIOReq Sleep) where showp = show++instance Hashable (ConcurrentIOReq Sleep a) where+ hashWithSalt s (Sleep n) = hashWithSalt s n
tests/TestBadDataSource.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE OverloadedStrings #-} module TestBadDataSource (tests) where
tests/TestExampleDataSource.hs view
@@ -1,4 +1,11 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE CPP, OverloadedStrings, RebindableSyntax, MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-} module TestExampleDataSource (tests) where import Haxl.Prelude as Haxl@@ -7,7 +14,6 @@ import Haxl.Core.Monad (unsafeLiftIO) import Haxl.Core -import qualified Data.HashMap.Strict as HashMap import Test.HUnit import Data.IORef import Data.Maybe@@ -26,7 +32,8 @@ let st = stateSet exstate stateEmpty -- Create the Env:- initEnv st ()+ env <- initEnv st ()+ return env{ flags = (flags env){ report = 2 } } tests = TestList [@@ -34,9 +41,12 @@ TestLabel "orderTest" orderTest, TestLabel "preCacheTest" preCacheTest, TestLabel "cachedComputationTest" cachedComputationTest,+ TestLabel "cacheResultTest" cacheResultTest, TestLabel "memoTest" memoTest, TestLabel "dataSourceExceptionTest" dataSourceExceptionTest,- TestLabel "dumpCacheAsHaskell" dumpCacheTest]+ TestLabel "dumpCacheAsHaskell" dumpCacheTest,+ TestLabel "fetchError" fetchError+ ] -- Let's test ExampleDataSource. @@ -52,14 +62,15 @@ -- Should be just one fetching round: Stats stats <- readIORef (statsRef env)+ putStrLn (ppStats (Stats stats)) assertEqual "rounds" 1 (length stats) -- With two fetches: assertBool "reqs" $- if | RoundStats { roundDataSources = m } : _ <- stats,- Just (DataSourceRoundStats { dataSourceFetches = 2 })- <- HashMap.lookup "ExampleDataSource" m -> True- | otherwise -> False+ case stats of+ [FetchStats{..}] ->+ fetchDataSource == "ExampleDataSource" && fetchBatchSize == 2+ _otherwise -> False -- Test side-effect ordering @@ -97,8 +108,8 @@ x <- runHaxl env $ do cacheRequest (CountAardvarks "xxx") (Right 3)- cacheRequest (ListWombats 100) (Right [1,2,3])- countAardvarks "xxx" + (length <$> listWombats 100)+ cacheRequest (ListWombats 1000000) (Right [1,2,3])+ countAardvarks "xxx" + (length <$> listWombats 1000000) assertEqual "preCacheTest1" x (3 + 3) y <- Control.Exception.try $ runHaxl env $ do@@ -126,6 +137,16 @@ stats <- readIORef (statsRef env) assertEqual "fetches" 3 (numFetches stats) +cacheResultTest = TestCase $ do+ env <- testEnv+ ref <- newIORef 0+ let request = cacheResult (CountAardvarks "ababa") $ do+ modifyIORef ref (+1)+ readIORef ref+ r <- runHaxl env $ (+) <$> request <*> request+ assertEqual "cacheResult" 2 r++ -- Pretend CountAardvarks is a request computed by some Haxl code memoTest = TestCase $ do env <- testEnv@@ -142,6 +163,18 @@ stats <- readIORef (statsRef env) assertEqual "fetches" 3 (numFetches stats)++-- Test that the FetchError gets returned properly, and that we have+-- a failure logged in the stats.+fetchError = TestCase $ do+ env <- testEnv+ r <- runHaxl env $ Haxl.try $+ (++) <$> listWombats 1000000 <*> listWombats 1000001+ assertBool "fetchError1" $ case r of+ Left FetchError{} -> True+ Right _ -> False+ Stats stats <- readIORef (statsRef env)+ assertEqual "fetchError2" 2 (sum [ fetchFailures | FetchStats{..} <- stats ]) dataSourceExceptionTest = TestCase $ do env <- testEnv
tests/TestMain.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE CPP, OverloadedStrings #-} module Main where
tests/TestTypes.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE DeriveDataTypeable #-}
tests/TestUtils.hs view
@@ -1,3 +1,9 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.+ {-# LANGUAGE OverloadedStrings #-} module TestUtils ( makeTestEnv@@ -40,11 +46,12 @@ id4 :: Haxl Id id4 = lookupInput "D" -makeTestEnv :: IO (Env UserEnv)-makeTestEnv = do- tao <- MockTAO.initGlobalState+makeTestEnv :: Bool -> IO (Env UserEnv)+makeTestEnv future = do+ tao <- MockTAO.initGlobalState future let st = stateSet tao stateEmpty- initEnv st testinput+ env <- initEnv st testinput+ return env { flags = (flags env) { report = 2 } } expectRoundsWithEnv :: (Eq a, Show a) => Int -> a -> Haxl a -> Env UserEnv -> Assertion@@ -54,14 +61,14 @@ stats <- readIORef (statsRef env) assertEqual "rounds" n (numRounds stats) -expectRounds :: (Eq a, Show a) => Int -> a -> Haxl a -> Assertion-expectRounds n result haxl = do- env <- makeTestEnv+expectRounds :: (Eq a, Show a) => Int -> a -> Haxl a -> Bool -> Assertion+expectRounds n result haxl future = do+ env <- makeTestEnv future expectRoundsWithEnv n result haxl env -expectFetches :: (Eq a, Show a) => Int -> Haxl a -> Assertion-expectFetches n haxl = do- env <- makeTestEnv+expectFetches :: (Eq a, Show a) => Int -> Haxl a -> Bool -> Assertion+expectFetches n haxl future = do+ env <- makeTestEnv future _ <- runHaxl env haxl stats <- readIORef (statsRef env) assertEqual "fetches" n (numFetches stats)
+ tests/WorkDataSource.hs view
@@ -0,0 +1,44 @@+-- Copyright (c) 2014-present, Facebook, Inc.+-- All rights reserved.+--+-- This source code is distributed under the terms of a BSD license,+-- found in the LICENSE file.++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}++module WorkDataSource (+ work,+ ) where++import Haxl.Prelude+import Prelude ()++import Haxl.Core+import Haxl.DataSource.ConcurrentIO++import Control.Exception+import Data.Hashable+import Data.Typeable++work :: Integer -> GenHaxl u Integer+work n = dataFetch (Work n)++data Work deriving Typeable+instance ConcurrentIO Work where+ data ConcurrentIOReq Work a where+ Work :: Integer -> ConcurrentIOReq Work Integer++ performIO (Work n) = evaluate (sum [1..n]) >> return n++deriving instance Eq (ConcurrentIOReq Work a)+deriving instance Show (ConcurrentIOReq Work a)++instance ShowP (ConcurrentIOReq Work) where showp = show++instance Hashable (ConcurrentIOReq Work a) where+ hashWithSalt s (Work n) = hashWithSalt s n