haxl 0.1.0.0 → 2.5.1.1
raw patch · 58 files changed
Files
- Haxl/Core.hs +130/−16
- Haxl/Core/CallGraph.hs +45/−0
- Haxl/Core/DataCache.hs +174/−73
- Haxl/Core/DataSource.hs +565/−0
- Haxl/Core/Env.hs +0/−65
- Haxl/Core/Exception.hs +150/−37
- Haxl/Core/Fetch.hs +698/−114
- Haxl/Core/Flags.hs +127/−0
- Haxl/Core/Memo.hs +475/−0
- Haxl/Core/Monad.hs +1137/−350
- Haxl/Core/Parallel.hs +153/−0
- Haxl/Core/Profile.hs +244/−0
- Haxl/Core/RequestStore.hs +118/−18
- Haxl/Core/Run.hs +290/−0
- Haxl/Core/Show1.hs +0/−15
- Haxl/Core/ShowP.hs +20/−0
- Haxl/Core/StateStore.hs +45/−23
- Haxl/Core/Stats.hs +313/−0
- Haxl/Core/Types.hs +0/−363
- Haxl/Core/Util.hs +27/−25
- Haxl/DataSource/ConcurrentIO.hs +83/−0
- Haxl/Prelude.hs +63/−37
- LICENSE +1/−1
- PATENTS +0/−23
- Setup.hs +9/−0
- changelog.md +109/−0
- haxl.cabal +134/−24
- readme.md +28/−10
- tests/AdoTests.hs +57/−0
- tests/AllTests.hs +50/−0
- tests/BadDataSource.hs +132/−0
- tests/BatchTests.hs +163/−82
- tests/Bench.hs +68/−0
- tests/CoreTests.hs +114/−6
- tests/DataCacheTest.hs +157/−17
- tests/DataSourceDispatchTests.hs +83/−0
- tests/ExampleDataSource.hs +31/−10
- tests/ExceptionStackTests.hs +73/−0
- tests/FullyAsyncTest.hs +67/−0
- tests/LoadCache.hs +8/−0
- tests/LoadCache.txt +1/−1
- tests/Main.hs +0/−20
- tests/MemoizationTests.hs +78/−0
- tests/MockTAO.hs +32/−10
- tests/MonadAsyncTest.hs +130/−0
- tests/MonadBench.hs +173/−0
- tests/OutgoneFetchesTests.hs +82/−0
- tests/ParallelTests.hs +81/−0
- tests/ProfileTests.hs +218/−0
- tests/SleepDataSource.hs +48/−0
- tests/StatsTests.hs +195/−0
- tests/TestBadDataSource.hs +130/−0
- tests/TestExampleDataSource.hs +96/−14
- tests/TestMain.hs +17/−0
- tests/TestTypes.hs +31/−8
- tests/TestUtils.hs +82/−0
- tests/WorkDataSource.hs +87/−0
- tests/WriteTests.hs +300/−0
Haxl/Core.hs view
@@ -1,24 +1,138 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ -- | Everything needed to define data sources and to invoke the--- engine. This module should not be imported by user code.-module Haxl.Core- ( module Haxl.Core.Env- , module Haxl.Core.Monad- , module Haxl.Core.Types+-- engine.+--+{-# LANGUAGE CPP #-}+module Haxl.Core (+ -- * The monad and operations+ GenHaxl (..), runHaxl, runHaxlWithWrites++ -- ** Env+ , Env(..), Caches, caches+ -- *** Operations in the monad+ , env, withEnv, withLabel+ -- *** Building the Env+ , initEnvWithData, initEnv, emptyEnv, sanitizeEnv+ -- *** Building the StateStore+ , StateStore, stateGet, stateSet, stateEmpty++ -- ** Writes inside the monad+ , tellWrite, tellWriteNoMemo++ -- ** Exceptions+ , throw, catch, catchIf, try, tryToHaxlException++ -- ** Data fetching and caching+ , dataFetch, uncachedRequest+ , cacheRequest, dupableCacheRequest, cacheResult, cacheResultWithShow+ , cachedComputation, preCacheComputation+ , dumpCacheAsHaskell++ -- ** Memoization+ , newMemo, newMemoWith, prepareMemo, runMemo+ , memo, memoUnique, memoize, memoize1, memoize2+ , memoFingerprint, MemoFingerprintKey(..)++ -- ** Conditionals+ , pAnd, pOr, unsafeChooseFirst++ -- ** Statistics+ , Stats(..)+ , FetchStats(..)+ , CallId+ , Microseconds+ , Timestamp+ , emptyStats+ , numFetches+ , ppStats+ , ppFetchStats+ , aggregateFetchBatches+ , Profile(..)+ , ProfileMemo(..)+ , ProfileFetch(..)+ , emptyProfile+ , ProfileLabel+ , ProfileKey+ , ProfileData(..)+ , emptyProfileData+ , AllocCount+ , LabelHitCount++ -- * Report flags+ , ReportFlag(..)+ , ReportFlags+ , defaultReportFlags+ , profilingReportFlags+ , setReportFlag+ , clearReportFlag+ , testReportFlag++ -- ** Flags+ , Flags(..)+ , defaultFlags+ , ifTrace+ , ifReport+ , ifProfiling++ -- * Building data sources+ , DataSource(..)+ , ShowP(..)+ , DataSourceName(..)+ , Request+ , BlockedFetch(..)+ , PerformFetch(..)+ , StateKey(..)+ , SchedulerHint(..)+ , FailureClassification(..)++ -- ** Result variables+ , ResultVar(..)+ , mkResultVar+ , putFailure+ , putResult+ , putSuccess+ , putResultFromChildThread+ , putResultWithStats+ , putResultWithStatsFromChildThread+ , DataSourceStats(..)++ -- ** Default fetch implementations+ , asyncFetch, asyncFetchWithDispatch, asyncFetchAcquireRelease+ , backgroundFetchSeq, backgroundFetchPar+ , backgroundFetchAcquireRelease, backgroundFetchAcquireReleaseMVar+ , stubFetch+ , syncFetch++ -- ** Utilities+ , except+ , setError+ , getMapFromRCMap++ -- * Exceptions , module Haxl.Core.Exception- , module Haxl.Core.StateStore- , module Haxl.Core.Show1++ -- * Recording the function callgraph+ , module Haxl.Core.CallGraph ) where -import Haxl.Core.Env+import Haxl.Core.CallGraph+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.Show1 (Show1(..))+import Haxl.Core.RequestStore (getMapFromRCMap)+import Haxl.Core.ShowP (ShowP(..)) import Haxl.Core.StateStore
+ Haxl/Core/CallGraph.hs view
@@ -0,0 +1,45 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}++module Haxl.Core.CallGraph where++import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+#if __GLASGOW_HASKELL__ < 804+import Data.Monoid+#endif+import Data.Text (Text)+import qualified Data.Text as Text++type ModuleName = Text++-- | An unqualified function+type Function = Text++-- | A qualified function+data QualFunction = QualFunction ModuleName Function deriving (Eq, Ord)++instance Show QualFunction where+ show (QualFunction mn nm) = Text.unpack $ mn <> Text.pack "." <> nm++-- | Represents an edge between a parent function which calls a child function+-- in the call graph+type FunctionCall = (QualFunction, QualFunction)++-- | An edge list which represents the dependencies between function calls+type CallGraph = ([FunctionCall], Map QualFunction Text)++-- | Used as the root of all function calls+mainFunction :: QualFunction+mainFunction = QualFunction "MAIN" "main"++emptyCallGraph :: CallGraph+emptyCallGraph = ([], Map.empty)
Haxl/Core/DataCache.hs view
@@ -1,113 +1,214 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-} --- | A cache mapping data requests to their results.+-- |+-- 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- , empty+ ( DataCache(..)+ , SubCache(..)+ , emptyDataCache+ , filter , insert+ , insertNotShowable+ , insertWithShow , lookup , showCache+ , readCache ) where -import Data.HashMap.Strict (HashMap)-import Data.Hashable-import Prelude hiding (lookup)-import Unsafe.Coerce-import qualified Data.HashMap.Strict as HashMap-import Data.Typeable.Internal-import Data.Maybe-import Control.Applicative hiding (empty)+import Prelude hiding (lookup, filter) import Control.Exception+import Unsafe.Coerce+import Data.Typeable+import Data.Hashable+import qualified Data.HashTable.IO as H -import Haxl.Core.Types+-- ---------------------------------------------------------------------------+-- 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.+-- | 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. ----- See the definition of 'ResultVar' for more details.--newtype DataCache = DataCache (HashMap TypeRep SubCache)+newtype DataCache res = DataCache (HashTable 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 =- forall req a . (Hashable (req a), Eq (req a), Show (req a), Show a) =>- SubCache ! (HashMap (req a) (ResultVar a))+--+data SubCache res =+ forall req a . (Hashable (req a), Eq (req a)) =>+ SubCache (req a -> String) (a -> String) !(HashTable (req a) (res a)) -- NB. the inner HashMap is strict, to avoid building up -- a chain of thunks during repeated insertions. +type HashTable k v = H.BasicHashTable k v+ -- | A new, empty 'DataCache'.-empty :: DataCache-empty = DataCache HashMap.empty+emptyDataCache :: IO (DataCache res)+emptyDataCache = DataCache <$> H.new -- | Inserts a request-result pair into the 'DataCache'. insert- :: (Hashable (r a), Typeable (r a), Eq (r a), Show (r a), Show a)- => r a+ :: (Hashable (req a), Typeable (req a), Eq (req a), Show (req a), Show a)+ => req a -- ^ Request- -> ResultVar a+ -> res a -- ^ Result- -> DataCache- -> DataCache+ -> DataCache res+ -> IO () -insert req result (DataCache m) =- DataCache $- HashMap.insertWith fn (typeOf req)- (SubCache (HashMap.singleton req result)) m- where- fn (SubCache new) (SubCache old) =- SubCache (unsafeCoerce new `HashMap.union` old)+insert = insertWithShow show show +-- | Inserts a request-result pair into the 'DataCache', without+-- requiring Show instances of the request or the result. The cache+-- cannot be subsequently used with `showCache`.+insertNotShowable+ :: (Hashable (req a), Typeable (req a), Eq (req a))+ => req a+ -- ^ Request+ -> res a+ -- ^ Result+ -> DataCache res+ -> IO ()++insertNotShowable = insertWithShow notShowable notShowable++-- | Inserts a request-result pair into the 'DataCache', with the given+-- functions used to show the request and result.+insertWithShow+ :: (Hashable (req a), Typeable (req a), Eq (req a))+ => (req a -> String)+ -- ^ Show function for request+ -> (a -> String)+ -- ^ Show function for result+ -> req a+ -- ^ Request+ -> res a+ -- ^ Result+ -> DataCache res+ -> IO ()++insertWithShow showRequest showResult request result (DataCache m) =+ H.mutateIO m (typeOf request) (mutate showRequest showResult request result)++notShowable :: a+notShowable = error "insertNotShowable"++-- | A mutation function for mutateIO. If the key doesn't exist in the top-level+-- cache, creates a new hashtable and inserts the request and result.+-- If the key exists, insert the request and result into the existing subcache,+-- replacing any existing mapping.+mutate :: (Hashable (req a), Typeable (req a), Eq (req a))+ => (req a -> String)+ -> (a -> String)+ -> req a+ -> res a+ -> Maybe (SubCache res)+ -> IO (Maybe (SubCache res), ())+mutate showRequest showResult request result Nothing = do+ newTable <- H.new+ H.insert newTable request result+ return (Just (SubCache showRequest showResult newTable), ())+mutate _ _ request result (Just sc@(SubCache _ _ oldTable)) = do+ H.insert oldTable (unsafeCoerce request) (unsafeCoerce result)+ return (Just sc, ())+ -- | Looks up the cached result of a request. lookup- :: Typeable (r a)- => r a+ :: Typeable (req a)+ => req a -- ^ Request- -> DataCache- -> Maybe (ResultVar a)+ -> DataCache res+ -> IO (Maybe (res a)) -lookup req (DataCache m) =- case HashMap.lookup (typeOf req) m of- Nothing -> Nothing- Just (SubCache sc) ->- unsafeCoerce (HashMap.lookup (unsafeCoerce req) sc)+lookup req (DataCache m) = do+ mbRes <- H.lookup m (typeOf req)+ case mbRes of+ Nothing -> return Nothing+ Just (SubCache _ _ sc) ->+ unsafeCoerce (H.lookup sc (unsafeCoerce req)) +filter+ :: forall res+ . (forall a. res a -> IO Bool)+ -> DataCache res+ -> IO (DataCache res)+filter pred (DataCache cache) = do+ cacheList <- H.toList cache+ filteredCache <- filterSubCache `mapM` cacheList+ DataCache <$> H.fromList filteredCache+ where+ filterSubCache+ :: (TypeRep, SubCache res)+ -> IO (TypeRep, SubCache res)+ filterSubCache (ty, SubCache showReq showRes hm) = do+ filteredList <- H.foldM go [] hm+ filteredSC <- H.fromList filteredList+ return (ty, SubCache showReq showRes filteredSC)+ where+ go res (request, rvar) = do+ predRes <- pred rvar+ return $ if predRes then (request, rvar):res else res+ -- | Dumps the contents of the cache, with requests and responses--- converted to 'String's using 'show'. The entries are grouped by--- 'TypeRep'.---+-- converted to 'String's using the supplied show functions. The+-- entries are grouped by 'TypeRep'. Note that this will fail if+-- 'insertNotShowable' has been used to insert any entries. showCache- :: DataCache+ :: 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)- where- goSubCache- :: (TypeRep,SubCache)- -> IO (TypeRep,[(String, Either SomeException String)])- goSubCache (ty, SubCache hmap) = do- elems <- catMaybes <$> mapM go (HashMap.toList hmap)- return (ty, elems)+showCache (DataCache cache) readRes = H.foldM goSubCache [] cache+ where+ goSubCache+ :: [(TypeRep, [(String, Either SomeException String)])]+ -> (TypeRep, SubCache res)+ -> IO [(TypeRep, [(String, Either SomeException String)])]+ goSubCache res (ty, SubCache showReq showRes hm) = do+ subCacheResult <- H.foldM go [] hm+ return $ (ty, subCacheResult):res+ where+ go res (request, rvar) = do+ maybe_r <- readRes rvar+ return $ case maybe_r of+ Nothing -> res+ Just (Left e) -> (showReq request, Left e) : res+ Just (Right result) ->+ (showReq request, Right (showRes result)) : res - go :: (Show (req a), Show a)- => (req a, ResultVar a)- -> IO (Maybe (String, Either SomeException String))- go (req, rvar) = do- maybe_r <- tryReadResult rvar- case maybe_r of- Nothing -> return Nothing- Just (Left e) -> return (Just (show req, Left e))- Just (Right result) -> return (Just (show req, Right (show result)))+-- | Dumps the contents of the cache responses to list+readCache+ :: forall res ret+ . DataCache res+ -> (forall a . res a -> IO ret)+ -> IO [(TypeRep, [Either SomeException ret])]+readCache (DataCache cache) readRes = H.foldM goSubCache [] cache+ where+ goSubCache+ :: [(TypeRep, [Either SomeException ret])]+ -> (TypeRep, SubCache res)+ -> IO [(TypeRep, [Either SomeException ret])]+ goSubCache res (ty, SubCache _showReq _showRes hm) = do+ subCacheResult <- H.foldM go [] hm+ return $ (ty, subCacheResult):res+ where+ go res (_request, rvar) = do+ r <- try $ readRes rvar+ return $ r : res
+ Haxl/Core/DataSource.hs view
@@ -0,0 +1,565 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# 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(..)+ , FailureClassification(..)++ -- * Result variables+ , ResultVar(..)+ , mkResultVar+ , putFailure+ , putResult+ , putResultFromChildThread+ , putSuccess+ , putResultWithStats+ , putResultWithStatsFromChildThread++ -- * Default fetch implementations+ , asyncFetch, asyncFetchWithDispatch+ , asyncFetchAcquireRelease+ , backgroundFetchSeq, backgroundFetchPar+ , backgroundFetchAcquireRelease+ , backgroundFetchAcquireReleaseMVar+ , stubFetch+ , syncFetch++ -- * Utilities+ , except+ , setError+ ) where++import Control.Exception+import Control.Monad+import Data.Hashable+import Data.Text (Text)+import Data.Kind (Type)+import Data.Typeable++import Haxl.Core.Exception+import Haxl.Core.Flags+import Haxl.Core.ShowP+import Haxl.Core.StateStore+import Haxl.Core.Stats+++import GHC.Conc ( newStablePtrPrimMVar+ , PrimMVar)+import Control.Concurrent ( threadCapability+ , forkOn+ , myThreadId )+import Control.Concurrent.MVar+import Foreign.StablePtr++-- ---------------------------------------------------------------------------+-- 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++ schedulerHintState :: Maybe (State req) -> u -> SchedulerHint req+ schedulerHintState _ u = schedulerHint u++ classifyFailure :: u -> req a -> SomeException -> FailureClassification+ classifyFailure _ _ _ = StandardFailure++class DataSourceName (req :: Type -> Type) 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 :: Type -> Type)+ = 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.++-- | Hints to the stats module about how to deal with these failures+data FailureClassification+ = StandardFailure+ | IgnoredForStatsFailure++-- | 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.+++-- | 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 -> Maybe DataSourceStats -> 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 -> Maybe DataSourceStats -> 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 Nothing++putResultWithStats+ :: ResultVar a -> Either SomeException a -> DataSourceStats -> IO ()+putResultWithStats (ResultVar io) res st = io res False (Just st)++putResultWithStatsFromChildThread+ :: ResultVar a -> Either SomeException a -> DataSourceStats -> IO ()+putResultWithStatsFromChildThread (ResultVar io) res st = io res True (Just st)++-- | 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 Nothing+ -- 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++backgroundFetchSeq, backgroundFetchPar+ :: (forall a. request a -> IO (Either SomeException a))+ -- ^ Run one request, will be run in a background thread++ -> State request+ -- ^ Currently unused.++ -> Flags+ -- ^ Currently unused.++ -> u+ -- ^ Currently unused.++ -> PerformFetch request++backgroundFetchSeq run _state _flags _si =+ BackgroundFetch $ \requests -> do+ (cap, _) <- threadCapability =<< myThreadId+ mask $ \restore -> void $ forkOn cap $ do+ let rethrow = rethrowFromBg requests+ restore (mapM_ runOne requests) `catch` rethrow+ where+ runOne (BlockedFetch request result) = do+ res <- run request+ putResultFromBg result res++backgroundFetchPar run _state _flags _si =+ BackgroundFetch $ \requests -> do+ (cap, _) <- threadCapability =<< myThreadId+ mapM_ (runOneInThread cap) requests+ where+ runOneInThread cap request = do+ mask $ \restore -> void $ forkOn cap $ do+ let rethrow = rethrowFromBg [request]+ restore (runOne request) `catch` rethrow+ runOne (BlockedFetch request result) = do+ res <- run request+ putResultFromBg result res+++{- |+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 fetchFn (BlockedFetch request result)+ = (putResult result =<<) <$> fetchFn service request++putResultFromBg :: ResultVar a -> Either SomeException a -> IO ()+putResultFromBg result r = do+ -- See comment on putResultFromChildThread+ -- We must set the allocation counter to 0 here in case there are more+ -- results in the batch.+ -- This is safe as we own this thread, and know that there+ -- is no allocation limits set.+ putResultFromChildThread result r+ setAllocationCounter 0++rethrowFromBg :: [BlockedFetch req] -> SomeException -> IO ()+rethrowFromBg requests e = do+ mapM_ (rethrow1bg e) requests+ rethrowAsyncExceptions e+ where+ rethrow1bg e (BlockedFetch _ result) =+ putResultFromBg result (Left e)+++backgroundFetchAcquireReleaseMVar+ :: IO service+ -- ^ Resource acquisition for this datasource++ -> (service -> IO ())+ -- ^ Resource release++ -> (service -> Int -> MVar () -> IO ())+ -- ^ Dispatch all the pending requests and when ready trigger the given mvar++ -> (service -> IO ())+ -- ^ Process all requests++ -> (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++backgroundFetchAcquireReleaseMVar+ acquire release dispatch process enqueue _state _flags _si =+ BackgroundFetch $ \requests -> do+ mvar <- newEmptyMVar+ mask $ \restore -> do+ (cap, _) <- threadCapability =<< myThreadId+ service <- acquire+ getResults <- (do+ results <- restore $ mapM (submit service) requests+ -- dispatch takes ownership of mvar, so we call it under `mask` to+ -- ensure that it can safely manage that resource.+ dispatch service cap mvar+ return (sequence_ results)) `onException` release service+ -- now spawn off a background thread to wait on the dispatch to finish+ _tid <- forkOn cap $ do+ takeMVar mvar+ -- todo: it is possible that we would want to do+ -- this processResults on the main scheduler thread for performance+ -- which might reduce thread switching, especially for large batches+ -- but for now this seems to work just fine+ let rethrow = rethrowFromBg requests+ _ <- finally+ (restore (process service >> getResults) `catch` rethrow)+ (release service `catch` rethrow)+ return ()+ return ()+ where+ submit service (BlockedFetch request result) =+ (putResultFromBg result =<<) <$> enqueue service request+++{- |+A version of 'backgroundFetchAcquireReleaseMVar' where the dispatch function+is given a 'StablePtr PrimMVar' which is more useful for C based APIs.+-}++backgroundFetchAcquireRelease+ :: IO service+ -- ^ Resource acquisition for this datasource++ -> (service -> IO ())+ -- ^ Resource release++ -> (service -> Int -> StablePtr PrimMVar -> IO ())+ -- ^ Dispatch all the pending requests and when ready trigger the given mvar++ -> (service -> IO ())+ -- ^ Process all requests++ -> (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++backgroundFetchAcquireRelease+ a r dispatch = backgroundFetchAcquireReleaseMVar+ a+ r+ (\s c mvar -> do+ sp <- newStablePtrPrimMVar mvar+ dispatch s c sp)
− Haxl/Core/Env.hs
@@ -1,65 +0,0 @@--- Copyright (c) 2014, 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 OverloadedStrings #-}---- | The Haxl monad environment.-module Haxl.Core.Env- ( Env(..)- , emptyEnv- , initEnv- , initEnvWithData- , caches- ) where--import Haxl.Core.DataCache as DataCache-import Haxl.Core.StateStore-import Haxl.Core.Types--import Data.IORef---- | The data we carry around in the Haxl monad.-data Env u = Env- { cacheRef :: IORef DataCache -- cached data fetches- , memoRef :: IORef DataCache -- memoized computations- , flags :: Flags- , userEnv :: u- , statsRef :: IORef Stats- , states :: StateStore- -- ^ Data sources and other components can store their state in- -- here. Items in this store must be instances of 'StateKey'.- }--type Caches = (IORef DataCache, IORef DataCache)--caches :: Env u -> Caches-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 -> IO (Env u)-initEnvWithData states e (cref, mref) = do- sref <- newIORef emptyStats- return Env- { cacheRef = cref- , memoRef = mref- , flags = defaultFlags- , userEnv = e- , states = states- , statsRef = sref- }---- | Initializes an environment with 'DataStates' and an input map.-initEnv :: StateStore -> u -> IO (Env u)-initEnv states e = do- cref <- newIORef DataCache.empty- mref <- newIORef DataCache.empty- initEnvWithData states e (cref,mref)---- | A new, empty environment.-emptyEnv :: u -> IO (Env u)-emptyEnv = initEnv stateEmpty
Haxl/Core/Exception.hs view
@@ -1,10 +1,13 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE CPP #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE DeriveDataTypeable #-}@@ -24,7 +27,10 @@ -- 'withDefault' to be useful, for example, you'll want your -- exceptions to be children of 'LogicError' or 'TransientError' as -- appropriate.-+--+-- Most users should import "Haxl.Core" instead of importing this+-- module directly.+-- module Haxl.Core.Exception ( HaxlException(..),@@ -38,6 +44,10 @@ logicErrorToException, logicErrorFromException, + LogicBug(..),+ logicBugToException,+ logicBugFromException,+ TransientError(..), transientErrorToException, transientErrorFromException,@@ -45,6 +55,7 @@ -- ** Internal exceptions CriticalError(..), DataSourceError(..),+ NonHaxlException(..), -- ** Logic exceptions NotFound(..),@@ -52,21 +63,30 @@ EmptyList(..), JSONError(..), InvalidParameter(..),+ MonadFail(..), -- ** Transient exceptions FetchError(..), -- * Exception utilities asHaxlException,-+ MiddleException(..),+ rethrowAsyncExceptions,+ tryWithRethrow, ) where -import Control.Exception+#if __GLASGOW_HASKELL__ >= 808+import Prelude hiding (MonadFail)+#endif+import Control.Exception as Exception import Data.Aeson+import Data.Binary (Binary) import Data.Typeable import Data.Text (Text)+import qualified Data.Text as Text import Haxl.Core.Util+import GHC.Stack -- | We have a 3-tiered hierarchy of exceptions, with 'HaxlException' at -- the top, and all Haxl exceptions as children of this. Users should@@ -76,8 +96,11 @@ -- -- ['InternalError'] Something is wrong with Haxl core. ----- ['LogicError'] Something is wrong with Haxl client code.+-- ['LogicBug'] Something is wrong with Haxl client code. --+-- ['LogicError'] Things that really should be return values, e.g.+-- NotFound.+-- -- ['TransientError'] Something is temporarily failing (usually in a fetch). -- -- These are not meant to be thrown (but likely be caught). Thrown@@ -86,30 +109,43 @@ -- (generic transient failure) or 'CriticalError' (internal failure). -- data HaxlException- = forall e. (Exception e, MiddleException e) => HaxlException e+ = forall e. (MiddleException e)+ => HaxlException+ (Maybe Stack) -- filled in with the call stack when thrown,+ -- if PROFILING is on+ e deriving (Typeable) +type Stack = [Text]+ -- hopefully this will get more informative in the future+ instance Show HaxlException where- show (HaxlException e) = show e+ show (HaxlException (Just stk@(_:_)) e) =+ show e ++ '\n' : renderStack (reverse $ map Text.unpack stk)+ show (HaxlException _ e) = show e instance Exception HaxlException -- | These need to be serializable to JSON to cross FFI boundaries. instance ToJSON HaxlException where- toJSON (HaxlException e) = object- [ "type" .= show (typeOf e)- , "name" .= eName e- , "txt" .= show e- ]+ toJSON (HaxlException stk e) = object fields+ where+ fields | Just s@(_:_) <- stk = ("stack" .= s) : rest+ | otherwise = rest+ rest =+ [ "type" .= show (typeOf e)+ , "name" .= eName e+ , "txt" .= show e+ ] haxlExceptionToException- :: (Exception e, MiddleException e) => e -> SomeException-haxlExceptionToException = toException . HaxlException+ :: (MiddleException e) => e -> SomeException+haxlExceptionToException = toException . HaxlException Nothing haxlExceptionFromException- :: (Exception e, MiddleException e) => SomeException -> Maybe e+ :: (MiddleException e) => SomeException -> Maybe e haxlExceptionFromException x = do- HaxlException a <- fromException x+ HaxlException _ a <- fromException x cast a class (Exception a) => MiddleException a where@@ -181,61 +217,103 @@ LogicError a <- fromException x cast a +data LogicBug = forall e . (Exception e) => LogicBug e+ deriving (Typeable)++deriving instance Show LogicBug++instance Exception LogicBug where+ toException = haxlExceptionToException+ fromException = haxlExceptionFromException++instance MiddleException LogicBug where+ eName (LogicBug e) = show $ typeOf e++logicBugToException :: (Exception e) => e -> SomeException+logicBugToException = toException . LogicBug++logicBugFromException+ :: (Exception e) => SomeException -> Maybe e+logicBugFromException x = do+ LogicBug a <- fromException x+ cast a+ ------------------------------------------------------------------------ -- Leaf exceptions. You should throw these. Or make your own. ------------------------------------------------------------------------ -- | Generic \"critical\" exception. Something internal is -- borked. Panic.-data CriticalError = CriticalError Text- deriving (Typeable, Show)+newtype CriticalError = CriticalError Text+ deriving (Typeable, Binary, Eq, Show) instance Exception CriticalError where toException = internalErrorToException fromException = internalErrorFromException +-- | Exceptions that are converted to HaxlException by+-- asHaxlException. Typically these will be pure exceptions,+-- e.g., the 'error' function in pure code, or a pattern-match+-- failure.+newtype NonHaxlException = NonHaxlException Text+ deriving (Typeable, Binary, Eq, Show)++instance Exception NonHaxlException where+ toException = internalErrorToException+ fromException = internalErrorFromException+ -- | Generic \"something was not found\" exception.-data NotFound = NotFound Text- deriving (Typeable, Show)+newtype NotFound = NotFound Text+ deriving (Typeable, Binary, Eq, Show) instance Exception NotFound where toException = logicErrorToException fromException = logicErrorFromException -- | Generic \"something had the wrong type\" exception.-data UnexpectedType = UnexpectedType Text- deriving (Typeable, Show)+newtype UnexpectedType = UnexpectedType Text+ deriving (Typeable, Eq, Show) instance Exception UnexpectedType where toException = logicErrorToException fromException = logicErrorFromException -- | Generic \"input list was empty\" exception.-data EmptyList = EmptyList Text- deriving (Typeable,Show)+newtype EmptyList = EmptyList Text+ deriving (Typeable, Eq, Show) instance Exception EmptyList where toException = logicErrorToException fromException = logicErrorFromException+ -- TODO: should be a child of LogicBug -- | Generic \"Incorrect assumptions about JSON data\" exception.-data JSONError = JSONError Text- deriving (Typeable, Show)+newtype JSONError = JSONError Text+ deriving (Typeable, Eq, Show) instance Exception JSONError where toException = logicErrorToException fromException = logicErrorFromException -- | Generic \"passing some invalid parameter\" exception.-data InvalidParameter = InvalidParameter Text- deriving (Typeable, Show)+newtype InvalidParameter = InvalidParameter Text+ deriving (Typeable, Eq, Show) instance Exception InvalidParameter where toException = logicErrorToException fromException = logicErrorFromException+ -- TODO: should be a child of LogicBug +-- | Generic \"fail was called\" exception.+newtype MonadFail = MonadFail Text+ deriving (Typeable, Eq, Show)++instance Exception MonadFail where+ toException = logicErrorToException+ fromException = logicErrorFromException+ -- | Generic transient fetching exceptions.-data FetchError = FetchError Text+newtype FetchError = FetchError Text deriving (Typeable, Eq, Show) instance Exception FetchError where@@ -243,7 +321,7 @@ fromException = transientErrorFromException -- | A data source did something wrong-data DataSourceError = DataSourceError Text+newtype DataSourceError = DataSourceError Text deriving (Typeable, Eq, Show) instance Exception DataSourceError where@@ -251,10 +329,45 @@ fromException = internalErrorFromException -- | Converts all exceptions that are not derived from 'HaxlException'--- into 'CriticalError', using 'show'.+-- into 'NonHaxlException', using 'show'. asHaxlException :: SomeException -> HaxlException asHaxlException e | Just haxl_exception <- fromException e = -- it's a HaxlException haxl_exception | otherwise =- HaxlException (InternalError (CriticalError (textShow e)))+ HaxlException Nothing (InternalError (NonHaxlException (textShow e)))++-- 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.+--+rethrowAsyncExceptions :: SomeException -> IO ()+rethrowAsyncExceptions e+ | Just SomeAsyncException{} <- fromException e = Exception.throw e+ | otherwise = return ()++tryWithRethrow :: IO a -> IO (Either SomeException a)+tryWithRethrow io =+ (Right <$> io) `catch` \e -> do rethrowAsyncExceptions e ; return (Left e)
Haxl/Core/Fetch.hs view
@@ -1,172 +1,756 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE DeriveDataTypeable #-} --- | Generic fetching infrastructure, used by 'Haxl.Core.Monad'.+-- | Implementation of data-fetching operations. Most users should+-- import "Haxl.Core" instead.+-- module Haxl.Core.Fetch- ( CacheResult(..)- , cached- , memoized+ ( dataFetch+ , dataFetchWithShow+ , dataFetchWithInsert+ , uncachedRequest+ , cacheResult+ , dupableCacheRequest+ , 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+#if __GLASGOW_HASKELL__ < 804+import Data.Monoid+#endif+import Data.Proxy+import Data.Typeable+import Data.Text (Text)+import Data.Kind (Type)+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.Env import Haxl.Core.Exception+import Haxl.Core.Flags+import Haxl.Core.Monad+import Haxl.Core.Profile import Haxl.Core.RequestStore-import Haxl.Core.Show1+import Haxl.Core.ShowP+import Haxl.Core.Stats import Haxl.Core.StateStore-import Haxl.Core.Types import Haxl.Core.Util -import Control.Exception-import Control.Monad-import Data.IORef-import Data.List-import Data.Time-import Text.Printf-import Data.Monoid-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Text as Text+-- -----------------------------------------------------------------------------+-- Data fetching and caching --- | 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. Env u -> RequestStore u -> IO ()-performFetches env reqs = do- let f = flags env- sref = statsRef env- jobs = contents reqs+-- | Possible responses when checking the cache.+data CacheResult u w a+ -- | The request hadn't been seen until now.+ = Uncached+ (ResultVar a)+ {-# UNPACK #-} !(IVar u w a)+ {-# UNPACK #-} !CallId - t0 <- getCurrentTime+ -- | The request has been seen before, but its result has not yet been+ -- fetched.+ | CachedNotFetched+ {-# UNPACK #-} !(IVar u w a)+ {-# UNPACK #-} !CallId + -- | The request has been seen before, and its result has already been+ -- fetched.+ | Cached (ResultVal a w)+ {-# UNPACK #-} !CallId+++-- | 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 w.+ (DataSource u r, Typeable (r a))+ => (r a -> String) -- See Note [showFn]+ -> (r a -> DataCacheItem u w a -> DataCache (DataCacheItem u w) -> IO ())+ -> Env u w -> r a -> IO (CacheResult u w a)+cachedWithInsert showFn insertFn env@Env{..} req = do let- roundstats =- [ (dataSourceName (getReq reqs), length reqs)- | BlockedFetches reqs <- jobs ]- where- getReq :: [BlockedFetch r] -> r a- getReq = undefined+ doFetch = do+ ivar <- newIVar+ k <- nextCallId env+ let !rvar = stdResultVar env ivar (Proxy :: Proxy r)+ insertFn req (DataCacheItem ivar k) dataCache+ return (Uncached rvar ivar k)+ mbRes <- DataCache.lookup req dataCache+ case mbRes of+ Nothing -> doFetch+ Just (DataCacheItem i@IVar{ivarRef = cr} k) -> do+ e <- readIORef cr+ case e of+ IVarEmpty _ -> do+ ivar <- withCurrentCCS i+ return (CachedNotFetched ivar k)+ 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 k) - modifyIORef' sref $ \(Stats rounds) ->- Stats (RoundStats (HashMap.fromList roundstats) : rounds) - ifTrace f 1 $- printf "Batch data fetch (%s)\n" $- intercalate (", "::String) $- map (\(name,num) -> printf "%d %s" num (Text.unpack name)) roundstats+-- | Make a ResultVar with the standard function for sending a CompletionReq+-- to the scheduler. This is the function will be executed when the fetch+-- completes.+stdResultVar+ :: forall r a u w. (DataSourceName r, Typeable r)+ => Env u w+ -> IVar u w a+ -> Proxy r+ -> ResultVar a+stdResultVar Env{..} ivar p =+ 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+ atomicallyOnBlocking+ (LogicBug (ReadingCompletionsFailedFetch (dataSourceName p))) $ do+ cs <- readTVar completions+ writeTVar completions (CompleteReq (eitherToResult r) ivar allocs : cs)+ -- Decrement the counter as request has finished. Do this after updating the+ -- completions TVar so that if the scheduler is tracking what was being+ -- waited on it gets a consistent view.+ ifReport flags ReportOutgoneFetches $+ atomicModifyIORef' submittedReqsRef (\m -> (subFromCountMap p 1 m, ()))+{-# INLINE stdResultVar #-} ++-- | Record the call stack for a data fetch in the Stats. Only useful+-- when profiling.+logFetch :: Env u w -> (r a -> String) -> r a -> CallId -> IO ()+#ifdef PROFILING+logFetch env showFn req fid = do+ ifReport (flags env) ReportFetchStack $ do+ stack <- currentCallStack+ modifyIORef' (statsRef env) $ \(Stats s) ->+ Stats (FetchCall (showFn req) stack fid : s)+#else+logFetch _ _ _ _ = return ()+#endif++calcFailure+ :: forall u req a . DataSource u req+ => u+ -> req a+ -> Either SomeException a+ -> FailureCount+calcFailure _u _r Right{} = mempty+calcFailure u r (Left e) = case classifyFailure u r e of+ StandardFailure -> mempty { failureCountStandard = 1 }+ IgnoredForStatsFailure -> mempty { failureCountIgnored = 1 }++addFallbackFetchStats+ :: forall u w req a . DataSource u req+ => Env u w+ -> CallId+ -> req a+ -> ResultVal a w+ -> IO ()+addFallbackFetchStats Env{..} fid req res = do+ bid <- atomicModifyIORef' statsBatchIdRef $ \x -> (x+1,x+1)+ start <- getTimestamp+ let+ dsName = dataSourceName (Proxy :: Proxy req)+ FailureCount{..} = case res of+ Ok{} -> mempty+ (ThrowHaxl e _) -> calcFailure userEnv req (Left e)+ (ThrowIO e) -> calcFailure userEnv req (Left e)+ this = FetchStats { fetchDataSource = dsName+ , fetchBatchSize = 1+ , fetchStart = start+ , fetchDuration = 0+ , fetchSpace = 0+ , fetchFailures = failureCountStandard+ , fetchIgnoredFailures = failureCountIgnored+ , fetchBatchId = bid+ , fetchIds = [fid] }+ atomicModifyIORef' statsRef $ \(Stats fs) -> (Stats (this : fs), ())++addFallbackResult+ :: Env u w+ -> ResultVal a w+ -> IVar u w a+ -> IO ()+addFallbackResult Env{..} res ivar = do+ atomicallyOnBlocking+ (LogicBug (ReadingCompletionsFailedFetch "addFallbackResult")) $ do+ cs <- readTVar completions+ writeTVar completions (CompleteReq res ivar 0 : cs)++-- | Performs actual fetching of data for a 'Request' from a 'DataSource'.+dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u w 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 w 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 w r a+ . (DataSource u r, Eq (r a), Hashable (r a), Typeable (r a))+ => (r a -> String) -- See Note [showFn]+ -> (r a -> DataCacheItem u w a -> DataCache (DataCacheItem u w) -> IO ())+ -> r a+ -> GenHaxl u w a+dataFetchWithInsert showFn insertFn req =+ GenHaxl $ \env@Env{..} -> do+ -- First, check the cache+ res <- cachedWithInsert showFn insertFn env req+ case res of+ -- This request has not been seen before+ Uncached rvar ivar fid -> do+ logFetch env showFn req fid+ ifProfiling flags $ addProfileFetch env req fid False+ --+ -- Check whether the data source wants to submit requests+ -- eagerly, or batch them up.+ --+ let+ blockedFetch = BlockedFetch req rvar+ blockedFetchI = BlockedFetchInternal fid+ submitFetch = do+ let hint :: SchedulerHint r+ hint = schedulerHintState (stateGet states) userEnv+ case hint of+ SubmitImmediately ->+ performFetches env [BlockedFetches [blockedFetch] [blockedFetchI]]+ TryToBatch ->+ -- add the request to the RequestStore and continue+ modifyIORef' reqStoreRef $ \bs ->+ addRequest blockedFetch blockedFetchI bs+ return $ Blocked ivar (Return ivar)++ -- if there is a fallback configured try that,+ -- else dispatch the fetch+ case dataCacheFetchFallback of+ Nothing -> submitFetch+ Just (DataCacheLookup dcl) -> do+ mbFallbackRes <- dcl req+ case mbFallbackRes of+ Nothing -> submitFetch+ Just fallbackRes -> do+ addFallbackResult env fallbackRes ivar+ ifReport flags ReportFetchStats $ addFallbackFetchStats+ env+ fid+ req+ fallbackRes+ return $ Blocked ivar (Return ivar)++ -- Seen before but not fetched yet. We're blocked, but we don't have+ -- to add the request to the RequestStore.+ CachedNotFetched ivar fid -> do+ ifProfiling flags $ addProfileFetch env req fid True+ return $ Blocked ivar (Return ivar)++ -- Cached: either a result, or an exception+ Cached r fid -> do+ ifProfiling flags $ addProfileFetch env req fid True+ done env 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+ :: forall a u w (r :: Type -> Type). (DataSource u r, Request r a)+ => r a -> GenHaxl u w a+uncachedRequest req = do+ flg <- env flags+ if recording flg /= 0+ then dataFetch req+ else GenHaxl $ \e@Env{..} -> do+ ivar <- newIVar+ k <- nextCallId e+ let !rvar = stdResultVar e ivar (Proxy :: Proxy r)+ modifyIORef' reqStoreRef $ \bs ->+ addRequest (BlockedFetch req rvar) (BlockedFetchInternal k) bs+ return $ Blocked ivar (Return 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 w 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 w 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 -> DataCacheItem u w a -> DataCache (DataCacheItem u w) -> IO ())+ -> r a+ -> IO a+ -> GenHaxl u w a+cacheResultWithInsert showFn insertFn req val = GenHaxl $ \env@Env{..} -> do+ mbRes <- DataCache.lookup req dataCache+ case mbRes of+ Nothing -> do+ let+ getResult = do+ eitherResult <- Exception.try val+ case eitherResult of+ Left e -> rethrowAsyncExceptions e+ _ -> return ()+ return $ eitherToResultThrowIO eitherResult+ -- if there is a fallback configured try that+ result <- case dataCacheFetchFallback of+ Nothing -> getResult+ Just (DataCacheLookup dcl) -> do+ mbFallbackRes <- dcl req+ maybe getResult return mbFallbackRes+ ivar <- newFullIVar result+ k <- nextCallId env+ insertFn req (DataCacheItem ivar k) dataCache+ done env result+ Just (DataCacheItem IVar{ivarRef = cr} _) -> do+ e <- readIORef cr+ case e of+ IVarEmpty _ -> raise env corruptCache+ IVarFull r -> done env r+ where+ corruptCache = 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 w ()+cacheRequest request result = GenHaxl $ \e@Env{..} -> do+ mbRes <- DataCache.lookup request dataCache+ case mbRes of+ Nothing -> do+ cr <- newFullIVar (eitherToResult result)+ k <- nextCallId e+ DataCache.insert request (DataCacheItem cr k) dataCache+ 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 e $+ DataSourceError "cacheRequest: request is already in the cache"++-- | Similar to @cacheRequest@ but doesn't throw an exception if the key+-- already exists in the cache.+-- If this function is called twice to cache the same Haxl request, the first+-- value will be discarded and overwritten with the second value.+-- Useful e.g. for unit tests+dupableCacheRequest+ :: Request req a => req a -> Either SomeException a -> GenHaxl u w ()+dupableCacheRequest request result = GenHaxl $ \e@Env{..} -> do+ cr <- newFullIVar (eitherToResult result)+ k <- nextCallId e+ DataCache.insert request (DataCacheItem cr k) dataCache+ return (Done ())++performRequestStore+ :: forall u w. Env u w -> RequestStore u -> IO ()+performRequestStore env reqStore =+ performFetches 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 w. Env u w -> [BlockedFetches u] -> IO ()+performFetches env@Env{flags=f, statsRef=sref, statsBatchIdRef=sbref} jobs = do+ t0 <- getTimestamp+ ifTrace f 3 $- forM_ jobs $ \(BlockedFetches reqs) ->- forM_ reqs $ \(BlockedFetch r _) -> putStrLn (show1 r)+ forM_ jobs $ \(BlockedFetches reqs _) ->+ forM_ reqs $ \(BlockedFetch r _) -> putStrLn (showp r) let- applyFetch (BlockedFetches (reqs :: [BlockedFetch r])) =+ applyFetch i bfs@(BlockedFetches (reqs :: [BlockedFetch r]) _) = case stateGet (states env) of Nothing ->- return (SyncFetch (mapM_ (setError (const e)) reqs))- where req :: r a; req = undefined- e = DataSourceError $- "data source not initialized: " <> dataSourceName req+ 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 $ wrapFetch reqs $ fetch state f (userEnv env) reqs+ return $ FetchToDo reqs+ $ (if testReportFlag ReportFetchStats $ report f+ then wrapFetchInStats+ (userEnv env)+ sref+ sbref+ dsName+ (length reqs)+ bfs+ else id)+ $ wrapFetchInTrace i (length reqs) dsName+ $ wrapFetchInCatch reqs+ $ fetch state f (userEnv env)+ where+ dsName = dataSourceName (Proxy :: Proxy r) - fetches <- mapM applyFetch jobs+ fetches <- zipWithM applyFetch [0..] jobs - scheduleFetches fetches+ scheduleFetches fetches (submittedReqsRef env) (flags env) - ifTrace f 1 $ do- t1 <- getCurrentTime- printf "Batch data fetch done (%.2fs)\n"- (realToFrac (diffUTCTime t1 t0) :: Double)+ t1 <- getTimestamp+ let roundtime = fromIntegral (t1 - t0) / 1000000 :: Double + ifTrace f 1 $+ printf "Batch data fetch done (%.4fs)\n" (realToFrac roundtime :: Double)++data FetchToDo where+ FetchToDo+ :: forall (req :: Type -> Type). (DataSourceName req, Typeable req)+ => [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. ---wrapFetch :: [BlockedFetch req] -> PerformFetch -> PerformFetch-wrapFetch reqs fetch =+wrapFetchInCatch :: [BlockedFetch req] -> PerformFetch req -> PerformFetch req+wrapFetchInCatch reqs fetch = case fetch of- SyncFetch io -> SyncFetch (io `catch` handler)- AsyncFetch fio -> AsyncFetch (\io -> fio io `catch` handler)+ 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.+ BackgroundFetch f ->+ BackgroundFetch $ \reqs -> f reqs `Exception.catch` handler where handler :: SomeException -> IO ()- handler e = mapM_ (forceError e) reqs+ 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+ forceError e (BlockedFetch _ rvar) = putResult rvar (except e) ++data FailureCount = FailureCount+ { failureCountStandard :: {-# UNPACK #-} !Int+ , failureCountIgnored :: {-# UNPACK #-} !Int+ }++#if __GLASGOW_HASKELL__ >= 804+instance Semigroup FailureCount where+ FailureCount s1 i1 <> FailureCount s2 i2 = FailureCount (s1+s2) (i1+i2)+#endif++instance Monoid FailureCount where+ mempty = FailureCount 0 0+#if __GLASGOW_HASKELL__ < 804+ mappend (FailureCount s1 i1) (FailureCount s2 i2)+ = FailureCount (s1+s2) (i1+i2)+#endif++wrapFetchInStats+ :: DataSource u req+ => u+ -> IORef Stats+ -> IORef Int+ -> Text+ -> Int+ -> BlockedFetches u+ -> PerformFetch req+ -> PerformFetch req+wrapFetchInStats+ u+ !statsRef+ !batchIdRef+ dataSource+ batchSize+ (BlockedFetches _reqs reqsI)+ perform = do+ case perform of+ SyncFetch f ->+ SyncFetch $ \reqs -> do+ bid <- newBatchId+ fail_ref <- newIORef mempty+ (t0,t,alloc,_) <- statsForIO (f (map (addFailureCount u fail_ref)+ (reqsWithFetchDsStats bid reqs)))+ failures <- readIORef fail_ref+ updateFetchStats bid allFids t0 t alloc batchSize failures+ AsyncFetch f -> do+ AsyncFetch $ \reqs inner -> do+ bid <- newBatchId+ inner_r <- newIORef (0, 0)+ fail_ref <- newIORef mempty+ let inner' = do+ (_,t,alloc,_) <- statsForIO inner+ writeIORef inner_r (t,alloc)+ reqs' = map (addFailureCount u fail_ref) reqs+ reqs'' = reqsWithFetchDsStats bid reqs'+ (t0, totalTime, totalAlloc, _) <- statsForIO (f reqs'' inner')+ (innerTime, innerAlloc) <- readIORef inner_r+ failures <- readIORef fail_ref+ updateFetchStats bid allFids t0 (totalTime - innerTime)+ (totalAlloc - innerAlloc) batchSize failures+ BackgroundFetch io -> do+ BackgroundFetch $ \reqs -> do+ bid <- newBatchId+ startTime <- getTimestamp+ io (reqsWithFetchDsStats bid+ (zipWith (addTimer u bid startTime) reqs reqsI))+ where+ allFids = map (\(BlockedFetchInternal k) -> k) reqsI+ newBatchId = atomicModifyIORef' batchIdRef $ \x -> (x+1,x+1)+ statsForIO io = do+ prevAlloc <- getAllocationCounter+ (t0,t,a) <- time io+ postAlloc <- getAllocationCounter+ return (t0,t, fromIntegral $ prevAlloc - postAlloc, a)+ reqsWithFetchDsStats = \bid reqs+ -> zipWith (addFetchDatasourceStats bid) reqs reqsI+ addTimer+ u+ bid+ t0+ (BlockedFetch req (ResultVar fn))+ (BlockedFetchInternal fid) =+ BlockedFetch req $ ResultVar $ \result isChildThread stats -> do+ t1 <- getTimestamp+ -- We cannot measure allocation easily for BackgroundFetch. Here we+ -- just attribute all allocation to the last+ -- `putResultFromChildThread` and use 0 for the others.+ -- While the individual allocations may not be correct,+ -- the total sum and amortized allocation are still meaningful.+ -- see Note [tracking allocation in child threads]+ allocs <- if isChildThread then getAllocationCounter else return 0+ updateFetchStats bid [fid] t0 (t1 - t0)+ (negate allocs)+ 1 -- batch size: we don't know if this is a batch or not+ (calcFailure u req result) -- failures+ fn result isChildThread stats++ addFetchDatasourceStats+ :: Int+ -> BlockedFetch r+ -> BlockedFetchInternal+ -> BlockedFetch r+ addFetchDatasourceStats bid+ (BlockedFetch req (ResultVar fn))+ (BlockedFetchInternal fid) = BlockedFetch req $ ResultVar+ $ \result isChildThread stats -> do+ let mkStats dss = FetchDataSourceStats+ { fetchDsStatsCallId = fid+ , fetchDsStatsDataSource = dataSource+ , fetchDsStatsStats = dss+ , fetchBatchId = bid+ }+ case stats of+ Just dss -> atomicModifyIORef' statsRef+ $ \(Stats fs) -> (Stats (mkStats dss : fs), ())+ Nothing -> return ()+ fn result isChildThread stats+++ updateFetchStats+ :: Int+ -> [CallId]+ -> Timestamp+ -> Microseconds+ -> Int64+ -> Int+ -> FailureCount+ -> IO ()+ updateFetchStats bid fids start time space batch FailureCount{..} = do+ let this = FetchStats { fetchDataSource = dataSource+ , fetchBatchSize = batch+ , fetchStart = start+ , fetchDuration = time+ , fetchSpace = space+ , fetchFailures = failureCountStandard+ , fetchIgnoredFailures = failureCountIgnored+ , fetchBatchId = bid+ , fetchIds = fids }+ atomicModifyIORef' statsRef $ \(Stats fs) -> (Stats (this : fs), ())++ addFailureCount :: DataSource u r+ => u -> IORef FailureCount -> BlockedFetch r -> BlockedFetch r+ addFailureCount u ref (BlockedFetch req (ResultVar fn)) =+ BlockedFetch req $ ResultVar $ \result isChildThread stats -> do+ let addFailures r = (r <> calcFailure u req result, ())+ when (isLeft result) $ atomicModifyIORef' ref addFailures+ fn result isChildThread stats++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 :: [PerformFetch] -> IO()-scheduleFetches fetches = async_fetches sync_fetches+scheduleFetches :: [FetchToDo] -> IORef ReqCountMap -> Flags -> IO ()+scheduleFetches fetches ref flags = do+ -- update ReqCountmap for these fetches+ ifReport flags ReportOutgoneFetches $ sequence_+ [ atomicModifyIORef' ref $+ \m -> (addToCountMap (Proxy :: Proxy r) (length reqs) m, ())+ | FetchToDo (reqs :: [BlockedFetch r]) _f <- fetches+ ]++ ifTrace flags 1 $ printf "Batch data fetch round: %s\n" $+ intercalate (", "::String) $+ map (\(c, n, ds) -> printf "%s %s %d" n ds c) stats++ fully_async_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]+ fetchName :: forall req . PerformFetch req -> String+ fetchName (BackgroundFetch _) = "background"+ fetchName (AsyncFetch _) = "async"+ fetchName (SyncFetch _) = "sync" --- | Possible responses when checking the cache.-data CacheResult a- -- | The request hadn't been seen until now.- = Uncached (ResultVar a)+ srcName :: forall req . (DataSourceName req) => [BlockedFetch req] -> String+ srcName _ = Text.unpack $ dataSourceName (Proxy :: Proxy req) - -- | The request has been seen before, but its result has not yet been- -- fetched.- | CachedNotFetched (ResultVar a)+ stats = [(length reqs, fetchName f, srcName reqs)+ | FetchToDo reqs f <- fetches] - -- | The request has been seen before, and its result has already been- -- fetched.- | Cached (Either SomeException a)+ fully_async_fetches :: IO ()+ fully_async_fetches = sequence_+ [f reqs | FetchToDo reqs (BackgroundFetch f) <- fetches] + async_fetches :: IO () -> IO ()+ async_fetches = compose+ [f reqs | FetchToDo reqs (AsyncFetch f) <- fetches] --- | Checks the data cache for the result of a request.-cached :: (Request r a) => Env u -> r a -> IO (CacheResult a)-cached env = checkCache (flags env) (cacheRef env)+ sync_fetches :: IO ()+ sync_fetches = sequence_+ [f reqs | FetchToDo reqs (SyncFetch f) <- fetches] --- | Checks the memo cache for the result of a computation.-memoized :: (Request r a) => Env u -> r a -> IO (CacheResult a)-memoized env = checkCache (flags env) (memoRef env) --- | Common guts of 'cached' and 'memoized'.-checkCache- :: (Request r a)- => Flags- -> IORef DataCache- -> r a- -> IO (CacheResult a)+-- | An exception thrown when reading from datasources fails+data ReadingCompletionsFailedFetch = ReadingCompletionsFailedFetch Text+ deriving Show -checkCache flags ref req = do- cache <- readIORef ref- let- do_fetch = do- rvar <- newEmptyResult- writeIORef ref (DataCache.insert req rvar cache)- return (Uncached rvar)- case DataCache.lookup req cache of- Nothing -> do_fetch- 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 3 $ putStrLn $ case r of- Left _ -> "Cached error: " ++ show req- Right _ -> "Cached request: " ++ show req- return (Cached r)+instance Exception ReadingCompletionsFailedFetch
+ Haxl/Core/Flags.hs view
@@ -0,0 +1,127 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE BangPatterns #-}++-- |+-- 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+ (+ -- * Report flags+ ReportFlag(..)+ , ReportFlags+ , defaultReportFlags+ , profilingReportFlags+ , setReportFlag+ , clearReportFlag+ , testReportFlag+ -- * Flags+ , Flags(..)+ , defaultFlags+ , ifTrace+ , ifReport+ , ifProfiling+ ) where++import Control.Monad+import Data.Bits+import Data.List (foldl')+import Text.Printf (printf)++-- ---------------------------------------------------------------------------+-- ReportFlags+data ReportFlag+ = ReportOutgoneFetches -- ^ outgone fetches, for debugging eg: timeouts+ | ReportFetchStats -- ^ data fetch stats & errors+ | ReportProfiling -- ^ enabling label stack and profiling+ | ReportExceptionLabelStack -- ^ include label stack in HaxlException+ | ReportFetchStack -- ^ log cost-center stack traces of dataFetch calls+ deriving (Bounded, Enum, Eq, Show)++profilingDependents :: [ReportFlag]+profilingDependents =+ [ ReportExceptionLabelStack+ , ReportFetchStack+ ]++newtype ReportFlags = ReportFlags Int++instance Show ReportFlags where+ show (ReportFlags fs) = printf "%0*b" (fromEnum maxReportFlag + 1) fs+ where+ maxReportFlag = maxBound :: ReportFlag++defaultReportFlags :: ReportFlags+defaultReportFlags = ReportFlags 0++profilingReportFlags :: ReportFlags+profilingReportFlags = foldl' (flip setReportFlag) defaultReportFlags+ [ ReportOutgoneFetches+ , ReportFetchStats+ , ReportProfiling+ ]++setReportFlag :: ReportFlag -> ReportFlags -> ReportFlags+setReportFlag f (ReportFlags fs) =+ ReportFlags $ setDependencies $ setBit fs $ fromEnum f+ where+ setDependencies+ | f `elem` profilingDependents = flip setBit $ fromEnum ReportProfiling+ | otherwise = id++clearReportFlag :: ReportFlag -> ReportFlags -> ReportFlags+clearReportFlag f (ReportFlags fs) =+ ReportFlags $ clearDependents $ clearBit fs $ fromEnum f+ where+ clearDependents z = case f of+ ReportProfiling -> foldl' clearBit z $ map fromEnum profilingDependents+ _ -> z++{-# INLINE testReportFlag #-}+testReportFlag :: ReportFlag -> ReportFlags -> Bool+testReportFlag !f (ReportFlags !fs) = testBit fs $ fromEnum f++-- ---------------------------------------------------------------------------+-- Flags++-- | Flags that control the operation of the engine.+data Flags = Flags+ { trace :: {-# UNPACK #-} !Int+ -- ^ Tracing level (0 = quiet, 3 = very verbose).+ , report :: {-# UNPACK #-} !ReportFlags+ -- ^ Report flags+ , 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 = defaultReportFlags+ , caching = 1+ , recording = 0+ }++-- | Runs an action if the tracing level is above the given threshold.+ifTrace :: Monad m => Flags -> Int -> m a -> m ()+ifTrace flags i = when (trace flags >= i) . void++-- | Runs an action if the ReportFlag is set.+ifReport :: Monad m => Flags -> ReportFlag -> m a -> m ()+ifReport flags i = when (testReportFlag i $ report flags) . void++ifProfiling :: Monad m => Flags -> m a -> m ()+ifProfiling flags = ifReport flags ReportProfiling
+ Haxl/Core/Memo.hs view
@@ -0,0 +1,475 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- 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+ , memoUnique++ -- * Local memoization+ , MemoVar+ , newMemo+ , newMemoWith+ , prepareMemo+ , runMemo+ ) where++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+import Data.Int+import Data.Word++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.Stats+import Haxl.Core.Profile+import Haxl.Core.Util (trace_)++-- -----------------------------------------------------------------------------+-- 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 w a.+ ( Eq (req a)+ , Hashable (req a)+ , Typeable (req a)+ , Monoid w)+ => req a -> GenHaxl u w a -> GenHaxl u w a++cachedComputation req haxl = GenHaxl $ \env@Env{..} -> do+ mbRes <- DataCache.lookup req memoCache+ case mbRes of+ Just (DataCacheItem ivar k) -> do+ ifProfiling flags $ do+ incrementMemoHitCounterFor env k True+ unHaxl (getIVarWithWrites ivar) env+ Nothing -> do+ ivar <- newIVar+ k <- nextCallId env+ -- no need to incremenetMemoHitCounter as execMemo will do it+ DataCache.insertNotShowable req (DataCacheItem ivar k) memoCache+ execMemoNowProfiled env haxl ivar k+++-- | 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 w a.+ ( Eq (req a)+ , Hashable (req a)+ , Typeable (req a)+ , Monoid w)+ => req a -> GenHaxl u w a -> GenHaxl u w a+preCacheComputation req haxl = GenHaxl $ \env@Env{..} -> do+ mbRes <- DataCache.lookup req memoCache+ case mbRes of+ Just _ -> return $ Throw $ toException $ InvalidParameter+ "preCacheComputation: key is already cached"+ Nothing -> do+ ivar <- newIVar+ k <- nextCallId env+ DataCache.insertNotShowable req (DataCacheItem ivar k) memoCache+ execMemoNowProfiled env haxl ivar k++-- -----------------------------------------------------------------------------+-- Memoization++newtype MemoVar u w a = MemoVar (IORef (MemoStatus u w a))++data MemoStatus u w a+ = MemoEmpty+ | MemoReady (GenHaxl u w a) CallId+ | MemoRun {-# UNPACK #-} !(IVar u w a) {-# UNPACK #-} !CallId++-- | 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 w (MemoVar u w 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 w a -> GenHaxl u w a -> GenHaxl u w ()+prepareMemo (MemoVar memoRef) memoCmp+ = GenHaxl $ \env -> do+ k <- nextCallId env+ writeIORef memoRef (MemoReady memoCmp k)+ return (Done ())++-- | Convenience function, combines @newMemo@ and @prepareMemo@.+newMemoWith :: GenHaxl u w a -> GenHaxl u w (MemoVar u w 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 :: Monoid w => MemoVar u w a -> GenHaxl u w a+runMemo (MemoVar memoRef) = GenHaxl $ \env -> do+ stored <- readIORef memoRef+ case stored of+ -- Memo was not prepared first; throw an exception.+ MemoEmpty -> trace_ "MemoEmpty " $+ raise env $ CriticalError "Attempting to run empty memo."+ -- Memo has been prepared but not run yet+ MemoReady cont k -> trace_ "MemoReady" $ do+ ivar <- newIVar+ writeIORef memoRef (MemoRun ivar k)+ execMemoNowProfiled env cont ivar k+ -- The memo has already been run, get (or wait for) for the result+ MemoRun ivar k -> trace_ "MemoRun" $ do+ ifProfiling (flags env) $ do+ incrementMemoHitCounterFor env k True+ unHaxl (getIVarWithWrites ivar) env++execMemoNowProfiled+ :: Monoid w+ => Env u w+ -> GenHaxl u w a+ -> IVar u w a+ -> CallId+ -> IO (Result u w a)+execMemoNowProfiled envOuter cont ivar cid =+ if not $ testReportFlag ReportProfiling $ report $ flags envOuter+ then execMemoNow envOuter cont ivar+ else do+ incrementMemoHitCounterFor envOuter cid False+ unHaxl+ (collectMemoData 0 $ GenHaxl $ \e -> execMemoNow e cont ivar)+ envOuter+ where+ addStats :: Env u w -> Int64 -> IO ()+ addStats env acc = modifyIORef' (statsRef env) $ \(Stats s) ->+ Stats (MemoCall cid acc : s)+ collectMemoData :: Int64 -> GenHaxl u w a -> GenHaxl u w a+ collectMemoData acc f = GenHaxl $ \env -> do+ a0 <- getAllocationCounter+ r <- unHaxl f env{memoKey=cid}+ a1 <- getAllocationCounter+ let newTotal = acc + (a0 - a1)+ ret <- case r of+ Done a -> do addStats env newTotal; return (Done a)+ Throw e -> do addStats env newTotal; return (Throw e)+ Blocked ivar k ->+ return (Blocked ivar (Cont (collectMemoData newTotal (toHaxl k))))+ setAllocationCounter a1+ return ret++execMemoNow :: Monoid w => Env u w -> GenHaxl u w a -> IVar u w a -> IO (Result u w a)+execMemoNow env cont ivar = do+ wlogs <- newIORef mempty+ let+ !menv = env { writeLogsRef = wlogs }+ -- use an env with empty writes, so we can memoize the extra+ -- writes done as part of 'cont'+ r <- Exception.try $ unHaxl cont menv++ case r of+ Left e -> trace_ ("execMemoNow: Left " ++ show e) $ do+ rethrowAsyncExceptions e+ putIVar ivar (ThrowIO e) env+ throwIO e+ Right (Done a) -> trace_ "execMemoNow: Done" $ do+ wt <- readIORef wlogs+ putIVar ivar (Ok a (Just wt)) env+ modifyIORef' (writeLogsRef env) (<> wt)+ return (Done a)+ Right (Throw ex) -> trace_ ("execMemoNow: Throw" ++ show ex) $ do+ wt <- readIORef wlogs+ putIVar ivar (ThrowHaxl ex (Just wt)) env+ modifyIORef' (writeLogsRef env) (<> wt)+ return (Throw ex)+ Right (Blocked ivar' cont) -> trace_ "execMemoNow: Blocked" $ do+ -- We "block" this memoized computation in the new environment 'menv', so+ -- that when it finishes, we can store all the write logs from the env+ -- in the IVar.+ addJob menv (toHaxl cont) ivar ivar'+ -- Now we call @getIVarWithWrites@ to populate the writes in the original+ -- environment 'env'.+ return (Blocked ivar (Cont (getIVarWithWrites ivar)))++-- -----------------------------------------------------------------------------+-- 1-ary and 2-ary memo functions++newtype MemoVar1 u w a b = MemoVar1 (IORef (MemoStatus1 u w a b))+newtype MemoVar2 u w a b c = MemoVar2 (IORef (MemoStatus2 u w a b c))++data MemoStatus1 u w a b+ = MemoEmpty1+ | MemoTbl1 (a -> GenHaxl u w b) (HashMap.HashMap a (MemoVar u w b))++data MemoStatus2 u w a b c+ = MemoEmpty2+ | MemoTbl2+ (a -> b -> GenHaxl u w c)+ (HashMap.HashMap a (HashMap.HashMap b (MemoVar u w c)))++newMemo1 :: GenHaxl u w (MemoVar1 u w a b)+newMemo1 = unsafeLiftIO $ MemoVar1 <$> newIORef MemoEmpty1++newMemoWith1 :: (a -> GenHaxl u w b) -> GenHaxl u w (MemoVar1 u w a b)+newMemoWith1 f = newMemo1 >>= \r -> prepareMemo1 r f >> return r++prepareMemo1 :: MemoVar1 u w a b -> (a -> GenHaxl u w b) -> GenHaxl u w ()+prepareMemo1 (MemoVar1 r) f+ = unsafeLiftIO $ writeIORef r (MemoTbl1 f HashMap.empty)++runMemo1 :: (Eq a, Hashable a, Monoid w) => MemoVar1 u w a b -> a -> GenHaxl u w 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 w (MemoVar2 u w a b c)+newMemo2 = unsafeLiftIO $ MemoVar2 <$> newIORef MemoEmpty2++newMemoWith2 :: (a -> b -> GenHaxl u w c) -> GenHaxl u w (MemoVar2 u w a b c)+newMemoWith2 f = newMemo2 >>= \r -> prepareMemo2 r f >> return r++prepareMemo2 :: MemoVar2 u w a b c -> (a -> b -> GenHaxl u w c) -> GenHaxl u w ()+prepareMemo2 (MemoVar2 r) f+ = unsafeLiftIO $ writeIORef r (MemoTbl2 f HashMap.empty)++runMemo2 :: (Eq a, Hashable a, Eq b, Hashable b, Monoid w)+ => MemoVar2 u w a b c+ -> a -> b -> GenHaxl u w 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+-- calculated once; the second and subsequent time it will be returned+-- immediately. It is the caller's responsibility to ensure that for+-- every two calls @memo key haxl@, if they have the same @key@ then+-- they compute the same result.+memo+ :: (Typeable a, Typeable k, Hashable k, Eq k, Monoid w)+ => k -> GenHaxl u w a -> GenHaxl u w a+memo key = cachedComputation (MemoKey key)++{-# RULES+"memo/Text" memo = memoText :: (Typeable a, Monoid w) =>+ Text -> GenHaxl u w a -> GenHaxl u w a+ #-}++{-# NOINLINE memo #-}++-- | Memoize a computation using its location and a Fingerprint. This ensures+-- uniqueness across computations.+memoUnique+ :: (Typeable a, Typeable k, Hashable k, Eq k, Monoid w)+ => MemoFingerprintKey a -> Text -> k -> GenHaxl u w a -> GenHaxl u w a+memoUnique fp label key = withLabel label . memo (fp, key)++{-# NOINLINE memoUnique #-}++data MemoKey k a where+ MemoKey :: (Typeable k, Hashable k, Eq k) => k -> MemoKey k a+ deriving Typeable++deriving instance Eq (MemoKey k a)++instance Hashable (MemoKey k a) where+ hashWithSalt s (MemoKey t) = hashWithSalt s t++-- An optimised memo key for Text keys. This is used automatically+-- when the key is Text, due to the RULES pragma above.++data MemoTextKey a where+ MemoText :: Text -> MemoTextKey a+ deriving Typeable++deriving instance Eq (MemoTextKey a)++instance Hashable (MemoTextKey a) where+ hashWithSalt s (MemoText t) = hashWithSalt s t++memoText :: (Typeable a, Monoid w) => Text -> GenHaxl u w a -> GenHaxl u w a+memoText key = withLabel key . cachedComputation (MemoText key)++-- | A memo key derived from a 128-bit MD5 hash. Do not use this directly,+-- it is for use by automatically-generated memoization.+data MemoFingerprintKey a where+ MemoFingerprintKey+ :: {-# UNPACK #-} !Word64+ -> {-# UNPACK #-} !Word64+ -> Addr# -> Addr#+ -> MemoFingerprintKey a+ deriving Typeable++deriving instance Eq (MemoFingerprintKey a)++instance Hashable (MemoFingerprintKey a) where+ hashWithSalt s (MemoFingerprintKey x _ _ _) =+ hashWithSalt s (fromIntegral x :: Int)++-- This is optimised for cheap call sites: when we have a call+--+-- memoFingerprint (MemoFingerprintKey 1234 5678 "module"# "name"#) e+--+-- then the MemoFingerprintKey constructor will be statically+-- allocated (with two 64-bit fields and pointers to cstrings for the names),+-- and shared by all calls to memo. So the memo call will not allocate,+-- unlike memoText.+--+{-# NOINLINE memoFingerprint #-}+memoFingerprint+ :: (Typeable a, Monoid w) => MemoFingerprintKey a -> GenHaxl u w a -> GenHaxl u w a+memoFingerprint key@(MemoFingerprintKey _ _ mnPtr nPtr) =+ withFingerprintLabel mnPtr nPtr . cachedComputation key++-- * Generic memoization machinery.++-- | Transform a Haxl computation into a memoized version of itself.+--+-- Given a Haxl computation, @memoize@ creates a version which stores its result+-- 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 :: Monoid w => GenHaxl u w a -> GenHaxl u w (GenHaxl u w a)+memoize a = runMemo <$> newMemoWith a++-- | Transform a 1-argument function returning a Haxl computation into a+-- memoized version of itself.+--+-- Given a function @f@ of type @a -> GenHaxl u w b@, @memoize1@ creates a version+-- which memoizes the results of @f@ in a table keyed by its argument, and+-- returns stored results on subsequent invocations with the same argument.+--+-- e.g.:+--+-- > allFriends :: [Int] -> GenHaxl u w [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@.+memoize1 :: (Eq a, Hashable a, Monoid w)+ => (a -> GenHaxl u w b)+ -> GenHaxl u w (a -> GenHaxl u w b)+memoize1 f = runMemo1 <$> newMemoWith1 f++-- | Transform a 2-argument function returning a Haxl computation, into a+-- memoized version of itself.+--+-- The 2-ary version of @memoize1@, see its documentation for details.+memoize2 :: (Eq a, Hashable a, Eq b, Hashable b, Monoid w)+ => (a -> b -> GenHaxl u w c)+ -> GenHaxl u w (a -> b -> GenHaxl u w c)+memoize2 f = runMemo2 <$> newMemoWith2 f
Haxl/Core/Monad.hs view
@@ -1,351 +1,1138 @@--- Copyright (c) 2014, 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 FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternGuards #-}-{-# LANGUAGE ScopedTypeVariables #-}---- | The implementation of the 'Haxl' monad.-module Haxl.Core.Monad (- -- * The monad- GenHaxl (..), runHaxl,- env,-- -- * Exceptions- throw, catch, try, tryToHaxlException,-- -- * Data fetching and caching- dataFetch, uncachedRequest,- cacheRequest, cacheResult, cachedComputation,- dumpCacheAsHaskell,-- -- * Unsafe operations- unsafeLiftIO, unsafeToHaxlException,- ) where--import Haxl.Core.Types-import Haxl.Core.Fetch-import Haxl.Core.Env-import Haxl.Core.Exception-import Haxl.Core.RequestStore-import Haxl.Core.Util-import Haxl.Core.DataCache--import qualified Data.Text as Text-import Control.Exception (Exception(..), SomeException)-import qualified Control.Exception-import Control.Applicative hiding (Const)-import GHC.Exts (IsString(..))-#if __GLASGOW_HASKELL__ < 706-import Prelude hiding (catch)-#endif-import Data.IORef-import Data.Monoid-import Text.Printf-import Text.PrettyPrint hiding ((<>))-import Control.Arrow (left)---- -------------------------------------------------------------------------------- | 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 (GenHaxl u a)--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))--instance Functor (GenHaxl u) where- fmap f m = pure f <*> m--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 (f' <*> return a'))- Throw e -> return (Blocked (f' <*> throw e))- Blocked a' -> return (Blocked (f' <*> a'))---- | Runs a 'Haxl' computation in an 'Env'.-runHaxl :: Env u -> GenHaxl u a -> IO a-runHaxl env (GenHaxl haxl) = do- ref <- newIORef noRequests- e <- haxl env ref- case e of- Done a -> return a- Throw e -> Control.Exception.throw e- Blocked cont -> do- bs <- readIORef ref- performFetches env bs- runHaxl env cont---- | Extracts data from the 'Env'.-env :: (Env u -> a) -> GenHaxl u a-env f = GenHaxl $ \env _ref -> return (Done (f env))---- -------------------------------------------------------------------------------- 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 = return . Throw . toException--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 (catch k h))--try :: Exception e => GenHaxl u a -> GenHaxl u (Either e a)-try haxl = (Right <$> haxl) `catch` (return . Left)---- -------------------------------------------------------------------------------- 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 `Control.Exception.catch` \e -> return (Throw e)- case r of- Blocked c -> return (Blocked (unsafeToHaxlException 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---- | Performs actual fetching of data for a 'Request' from a 'DataSource'.-dataFetch :: (DataSource u r, Request r a) => r a -> GenHaxl u a-dataFetch req = GenHaxl $ \env ref -> do- -- First, check the cache- res <- cached 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- modifyIORef' ref $ \bs -> addRequest (BlockedFetch req rvar) bs- return $ Blocked (continueFetch 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 (continueFetch req rvar)-- -- Cached: either a result, or an exception- Cached (Left ex) -> return (Throw ex)- Cached (Right a) -> return (Done a)---- | 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, Request 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 (continueFetch req rvar)--continueFetch- :: (DataSource u r, Request r a, Show a)- => r a -> ResultVar a -> GenHaxl u a-continueFetch req rvar = GenHaxl $ \_env _ref -> do- m <- tryReadResult rvar- case m of- Nothing -> raise . DataSourceError $- textShow req <> " did not set contents of result var"- Just (Left e) -> return (Throw e)- Just (Right a) -> return (Done a)---- | Transparently provides caching. Useful for datasources that can--- return immediately, but also caches values.-cacheResult :: (Request r a) => r a -> IO a -> GenHaxl u a-cacheResult req val = GenHaxl $ \env _ref -> do- cachedResult <- cached env req- case cachedResult of- Uncached rvar -> do- result <- Control.Exception.try val- putResult rvar result- done result- Cached result -> done result- CachedNotFetched _ -> corruptCache- where- corruptCache = raise . DataSourceError $ Text.concat- [ textShow 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)---- | '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. (Request req a)- => req a -> GenHaxl u a -> GenHaxl u a-cachedComputation req haxl = GenHaxl $ \env ref -> do- res <- memoized env req- case res of- -- Uncached: we must compute the result and store it in the ResultVar.- Uncached rvar -> do- let- with_result :: Either SomeException a -> GenHaxl u a- with_result r = GenHaxl $ \_ _ -> do putResult rvar r; done r-- unHaxl (try haxl >>= with_result) env ref-- -- CachedNotFetched: this request is already being computed, we just- -- have to block until the result is available. Note that we might- -- have to block repeatedly, because the Haxl computation might block- -- multiple times before it has a result.- CachedNotFetched rvar -> return $ Blocked (continueCached rvar)- Cached r -> done r--continueCached :: ResultVar a -> GenHaxl u a-continueCached rvar = GenHaxl $ \_env _ref -> do- m <- tryReadResult rvar- case m of- -- Unlike dataFetch, Nothing is not an error here: the computation- -- is being worked on elsewhere and probably got blocked in a- -- datafetch, we just have to keep waiting for the result.- Nothing -> return $ Blocked (continueCached rvar)- Just r -> done r---- | 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 = 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 "loadCache :: GenHaxl u ()" $$- text "loadCache = do" $$- nest 2 (vcat (map mk_cr (concatMap snd entries))) $$+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{- 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 MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- 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(..)++ -- * Writes (for debugging only)+ , WriteTree(..)+ , tellWrite+ , tellWriteNoMemo+ , write+ , writeNoMemo+ , flattenWT+ , appendWTs+ , mbModifyWLRef+ , mapWrites+ , mapWriteTree++ -- * Cont+ , Cont(..)+ , toHaxl++ -- * IVar+ , IVar(..)+ , IVarContents(..)+ , newIVar+ , newFullIVar+ , withCurrentCCS+ , getIVar+ , getIVarWithWrites+ , putIVar++ -- * ResultVal+ , ResultVal(..)+ , done+ , eitherToResult+ , eitherToResultThrowIO++ -- * CompleteReq+ , CompleteReq(..)++ -- * Env+ , Env(..)+ , DataCacheItem(..)+ , DataCacheLookup(..)+ , HaxlDataCache+ , Caches+ , caches+ , initEnvWithData+ , initEnv+ , emptyEnv+ , env, withEnv+ , nextCallId+ , sanitizeEnv++ -- * Profiling+ , ProfileCurrent(..)++ -- * JobList+ , JobList(..)+ , appendJobList+ , lengthJobList+ , addJob++ -- * Exceptions+ , throw+ , raise+ , catch+ , catchIf+ , try+ , tryToHaxlException++ -- * Dumping the cache+ , dumpCacheAsHaskell+ , dumpCacheAsHaskellFn++ -- * CallGraph+#ifdef PROFILING+ , withCallGraph+#endif++ -- * 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 Haxl.Core.Util (trace_)++import Control.Applicative (liftA2)+import Control.Arrow (left)+import Control.Concurrent.STM+import qualified Control.Monad.Catch as Catch+import Control.Exception (Exception(..), SomeException, throwIO)+#if __GLASGOW_HASKELL__ >= 808+import Control.Monad hiding (MonadFail)+import qualified Control.Monad as CTL+#else+import Control.Monad+#endif+import qualified Control.Exception as Exception+import Data.Either (rights)+import Data.IORef+import Data.Int+import Data.Maybe+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NonEmpty+#if __GLASGOW_HASKELL__ < 804+import Data.Semigroup+#endif+import qualified Data.Text as Text+import Data.Typeable+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 qualified Data.Map as Map+import Data.Text (Text)+import Foreign.Ptr (Ptr)+import GHC.Stack+import Haxl.Core.CallGraph+#endif++-- -----------------------------------------------------------------------------+-- The environment++-- | The data we carry around in the Haxl monad.++data DataCacheItem u w a = DataCacheItem (IVar u w a) {-# UNPACK #-} !CallId+type HaxlDataCache u w = DataCache (DataCacheItem u w)+newtype DataCacheLookup w =+ DataCacheLookup+ (forall req a . Typeable (req a)+ => req a+ -> IO (Maybe (ResultVal a w)))++data Env u w = Env+ { dataCache :: {-# UNPACK #-} !(HaxlDataCache u w)+ -- ^ cached data fetches++ , memoCache :: {-# UNPACK #-} !(HaxlDataCache u w)+ -- ^ memoized computations++ , memoKey :: {-# UNPACK #-} !CallId+ -- ^ current running memo key++ , 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'.++ , statsBatchIdRef :: {-# UNPACK #-} !(IORef Int)+ -- ^ keeps track of a Unique ID for each batch dispatched with stats+ -- enabled, for aggregating after.++ , callIdRef :: {-# UNPACK #-} !(IORef CallId)+ -- ^ keeps track of a Unique ID for each fetch/memo.++ , profCurrent :: ProfileCurrent+ -- ^ 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 w))+ -- ^ 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.++ , submittedReqsRef :: {-# UNPACK #-} !(IORef ReqCountMap)+ -- ^ all outgone fetches which haven't yet returned. Entries are+ -- removed from this map as the fetches finish. This field is+ -- useful for tracking outgone fetches to detect downstream+ -- failures.++ , completions :: {-# UNPACK #-} !(TVar [CompleteReq u w])+ -- ^ 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.++ , writeLogsRef :: {-# UNPACK #-} !(IORef w)+ -- ^ A log of all writes done as part of this haxl computation. Any+ -- haxl computation that needs to be memoized runs in its own+ -- environment so that we can get a hold of those writes and put them+ -- in the IVar associated with the compuatation.+ , writeLogsRefNoMemo :: {-# UNPACK #-} !(IORef w)+ -- ^ This is just a specialized version of @writeLogsRef@, where we put+ -- logs that user doesn't want memoized. This is a better alternative to+ -- doing arbitrary IO from a (memoized) Haxl computation.++ , dataCacheFetchFallback :: !(Maybe (DataCacheLookup w))+ -- ^ Allows you to inject a DataCache lookup just before a dataFetch is+ -- dispatched. This is useful for injecting fetch results in testing.++#ifdef PROFILING+ , callGraphRef :: Maybe (IORef CallGraph)+ -- ^ An edge list representing the current function call graph. The type+ -- is wrapped in a Maybe to avoid changing the existing callsites.++ , currFunction :: QualFunction+ -- ^ The most recent function call.+#endif+ }++data ProfileCurrent = ProfileCurrent+ { profCurrentKey :: {-# UNPACK #-} !ProfileKey+ , profLabelStack :: {-# UNPACK #-} !(NonEmpty ProfileLabel)+ }++type Caches u w = (HaxlDataCache u w, HaxlDataCache u w)++caches :: Env u w -> Caches u w+caches env = (dataCache env, memoCache env)++getMaxCallId :: HaxlDataCache u w -> IO (Maybe Int)+getMaxCallId c = do+ callIds <- rights . concatMap snd <$>+ DataCache.readCache c (\(DataCacheItem _ i) -> return i)+ case callIds of+ [] -> return Nothing+ vals -> return $ Just (maximum vals)+++-- | Initialize an environment with a 'StateStore', an input map, a+-- preexisting 'DataCache', and a seed for the random number generator.+initEnvWithData :: Monoid w => StateStore -> u -> Caches u w -> IO (Env u w)+initEnvWithData states e (dcache, mcache) = do+ newCid <- max <$>+ (maybe 0 ((+) 1) <$> getMaxCallId dcache) <*>+ (maybe 0 ((+) 1) <$> getMaxCallId mcache)+ ciref<- newIORef newCid+ sref <- newIORef emptyStats+ sbref <- newIORef 0+ pref <- newIORef emptyProfile+ rs <- newIORef noRequests -- RequestStore+ rq <- newIORef JobNil -- RunQueue+ sr <- newIORef emptyReqCounts -- SubmittedReqs+ comps <- newTVarIO [] -- completion queue+ wl <- newIORef mempty+ wlnm <- newIORef mempty+ return Env+ { dataCache = dcache+ , memoCache = mcache+ , memoKey = (-1)+ , flags = defaultFlags+ , userEnv = e+ , states = states+ , statsRef = sref+ , statsBatchIdRef = sbref+ , profCurrent = ProfileCurrent 0 $ "MAIN" :| []+ , callIdRef = ciref+ , profRef = pref+ , reqStoreRef = rs+ , runQueueRef = rq+ , submittedReqsRef = sr+ , completions = comps+ , writeLogsRef = wl+ , writeLogsRefNoMemo = wlnm+ , dataCacheFetchFallback = Nothing+#ifdef PROFILING+ , callGraphRef = Nothing+ , currFunction = mainFunction+#endif+ }++-- | Initializes an environment with 'StateStore' and an input map.+initEnv :: Monoid w => StateStore -> u -> IO (Env u w)+initEnv states e = do+ dcache <- emptyDataCache+ mcache <- emptyDataCache+ initEnvWithData states e (dcache, mcache)++-- | A new, empty environment.+emptyEnv :: Monoid w => u -> IO (Env u w)+emptyEnv = initEnv stateEmpty++-- | If you're using the env from a failed Haxl computation in a second Haxl+-- computation, it is recommended to sanitize the Env to remove all empty+-- IVars - especially if it's possible the first Haxl computation could've+-- been interrupted via an async exception. This is because if the Haxl+-- computation was interrupted by an exception, it's possible that there are+-- entries in the cache which are still blocked, while the results from+-- outgone fetches have been discarded.+sanitizeEnv :: Env u w -> IO (Env u w)+sanitizeEnv env@Env{..} = do+ sanitizedDC <- DataCache.filter isIVarFull dataCache+ sanitizedMC <- DataCache.filter isIVarFull memoCache+ rs <- newIORef noRequests -- RequestStore+ rq <- newIORef JobNil -- RunQueue+ comps <- newTVarIO [] -- completion queue+ sr <- newIORef emptyReqCounts -- SubmittedReqs+ return env+ { dataCache = sanitizedDC+ , memoCache = sanitizedMC+ , reqStoreRef = rs+ , runQueueRef = rq+ , completions = comps+ , submittedReqsRef = sr+ }+ where+ isIVarFull (DataCacheItem IVar{..} _) = do+ ivarContents <- readIORef ivarRef+ case ivarContents of+ IVarFull _ -> return True+ _ -> return False++-- -----------------------------------------------------------------------------+-- WriteTree++-- | A tree of writes done during a Haxl computation. We could use a simple+-- list, but this allows us to avoid multiple mappends when concatenating+-- writes from two haxl computations.+--+-- Users should try to treat this data type as opaque, and prefer+-- to use @flattenWT@ to get a simple list of writes from a @WriteTree@.+data WriteTree w+ = NilWrites+ | SomeWrite w+ | MergeWrites (WriteTree w) (WriteTree w)+ deriving (Show)++instance Semigroup (WriteTree w) where+ (<>) = appendWTs++instance Monoid (WriteTree w) where+ mempty = NilWrites++appendWTs :: WriteTree w -> WriteTree w -> WriteTree w+appendWTs NilWrites w = w+appendWTs w NilWrites = w+appendWTs w1 w2 = MergeWrites w1 w2++-- This function must be called at the end of the Haxl computation to get+-- a list of writes.+-- Haxl provides no guarantees on the order of the returned logs.+flattenWT :: WriteTree w -> [w]+flattenWT = go []+ where+ go !ws NilWrites = ws+ go !ws (SomeWrite w) = w : ws+ go !ws (MergeWrites w1 w2) = go (go ws w2) w1++-- This is a convenience wrapper over modifyIORef, which only modifies+-- writeLogsRef IORef, for non NilWrites.+mbModifyWLRef :: WriteTree w -> IORef (WriteTree w) -> IO ()+mbModifyWLRef NilWrites _ = return ()+mbModifyWLRef !wt ref = modifyIORef' ref (`appendWTs` wt)++mapWriteTree :: (w -> w) -> WriteTree w -> WriteTree w+mapWriteTree _ NilWrites = NilWrites+mapWriteTree f (SomeWrite w) = SomeWrite (f w)+mapWriteTree f (MergeWrites wt1 wt2) =+ MergeWrites (mapWriteTree f wt1) (mapWriteTree f wt2)++-- -----------------------------------------------------------------------------+-- | 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 writer monad for 'WriteTree'. These can be used to do+-- arbitrary "logs" from any Haxl computation. These are better than+-- doing arbitrary IO from a Haxl computation as these writes also get+-- memoized if the Haxl computation associated with them is memoized.+-- Now if this memoized computation is run again, you'll get the writes+-- twice.++-- * 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 w a = GenHaxl+ { unHaxl :: Env u w -> IO (Result u w a) }++tellWrite :: w -> GenHaxl u (WriteTree w) ()+tellWrite = write . SomeWrite++write :: Monoid w => w -> GenHaxl u w ()+write wt = GenHaxl $ \Env{..} -> do+ modifyIORef' writeLogsRef (<> wt)+ return $ Done ()++tellWriteNoMemo :: w -> GenHaxl u (WriteTree w) ()+tellWriteNoMemo = writeNoMemo . SomeWrite++writeNoMemo :: Monoid w => w -> GenHaxl u w ()+writeNoMemo wt = GenHaxl $ \Env{..} -> do+ modifyIORef' writeLogsRefNoMemo (<> wt)+ return $ Done ()+++instance IsString a => IsString (GenHaxl u w 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 w+ = JobNil+ | forall a . JobCons+ (Env u w) -- See Note [make withEnv work] below.+ (GenHaxl u w a)+ {-# UNPACK #-} !(IVar u w a)+ (JobList u w)++-- 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 w -> JobList u w -> JobList u w+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 w -> 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.+#ifdef PROFILING+data IVar u w a = IVar+ { ivarRef :: {-# UNPACK #-} !(IORef (IVarContents u w a))+ , ivarCCS :: {-# UNPACK #-} !(Ptr CostCentreStack)+#else+newtype IVar u w a = IVar+ { ivarRef :: IORef (IVarContents u w a)+#endif+ }++data IVarContents u w a+ = IVarFull (ResultVal a w)+ | IVarEmpty (JobList u w)+ -- morally this is a list of @a -> GenHaxl u w ()@, 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 w a)+newIVar = do+ ivarRef <- newIORef (IVarEmpty JobNil)+#ifdef PROFILING+ ivarCCS <- getCurrentCCS ivarRef+#endif+ return IVar{..}++newFullIVar :: ResultVal a w -> IO (IVar u w a)+newFullIVar r = do+ ivarRef <- newIORef (IVarFull r)+#ifdef PROFILING+ ivarCCS <- getCurrentCCS ivarRef+#endif+ return IVar{..}++withCurrentCCS :: IVar u w a -> IO (IVar u w a)+#ifdef PROFILING+withCurrentCCS ivar = do+ ccs <- getCurrentCCS ivar+ return ivar{ivarCCS = ccs}+#else+withCurrentCCS = return+#endif++getIVar :: IVar u w a -> GenHaxl u w a+getIVar i@IVar{ivarRef = !ref} = GenHaxl $ \env -> do+ e <- readIORef ref+ case e of+ IVarFull (Ok a _wt) -> return (Done a)+ IVarFull (ThrowHaxl e _wt) -> raiseFromIVar env i e+ IVarFull (ThrowIO e) -> throwIO e+ IVarEmpty _ -> return (Blocked i (Return i))++-- Just a specialised version of getIVar, for efficiency in <*>+getIVarApply :: IVar u w (a -> b) -> a -> GenHaxl u w b+getIVarApply i@IVar{ivarRef = !ref} a = GenHaxl $ \env -> do+ e <- readIORef ref+ case e of+ IVarFull (Ok f _wt) -> return (Done (f a))+ IVarFull (ThrowHaxl e _wt) -> raiseFromIVar env i e+ IVarFull (ThrowIO e) -> throwIO e+ IVarEmpty _ ->+ return (Blocked i (Cont (getIVarApply i a)))++-- Another specialised version of getIVar, for efficiency in cachedComputation+getIVarWithWrites :: Monoid w => IVar u w a -> GenHaxl u w a+getIVarWithWrites i@IVar{ivarRef = !ref} = GenHaxl $ \env@Env{..} -> do+ e <- readIORef ref+ case e of+ IVarFull (Ok a wt) -> do+ modifyIORef' writeLogsRef (<> fromMaybe mempty wt)+ return (Done a)+ IVarFull (ThrowHaxl e wt) -> do+ modifyIORef' writeLogsRef (<> fromMaybe mempty wt)+ raiseFromIVar env i e+ IVarFull (ThrowIO e) -> throwIO e+ IVarEmpty _ ->+ return (Blocked i (Cont (getIVarWithWrites i)))++putIVar :: IVar u w a -> ResultVal a w -> Env u w -> IO ()+putIVar IVar{ivarRef = !ref} a Env{..} = do+ e <- readIORef ref+ case e of+ IVarEmpty jobs -> do+ writeIORef ref (IVarFull a)+ modifyIORef' runQueueRef (appendJobList jobs)+ -- An IVar is typically only meant to be written to once+ -- so it would make sense to throw an error here. But there+ -- are legitimate use-cases for writing several times.+ -- (See Haxl.Core.Parallel)+ IVarFull{} -> return ()++{-# INLINE addJob #-}+addJob :: Env u w -> GenHaxl u w b -> IVar u w b -> IVar u w a -> IO ()+addJob env !haxl !resultIVar IVar{ivarRef = !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 w+ = Ok a (Maybe w)+ | ThrowHaxl SomeException (Maybe w)+ | ThrowIO SomeException+ -- we get no write logs when an IO exception occurs++done :: Env u w -> ResultVal a w -> IO (Result u w a)+done _ (Ok a _) = return (Done a)+done env (ThrowHaxl e _) = raise env e+done _ (ThrowIO e) = throwIO e++eitherToResultThrowIO :: Either SomeException a -> ResultVal a w+eitherToResultThrowIO (Right a) = Ok a Nothing+eitherToResultThrowIO (Left e)+ | Just HaxlException{} <- fromException e = ThrowHaxl e Nothing+ | otherwise = ThrowIO e++eitherToResult :: Either SomeException a -> ResultVal a w+eitherToResult (Right a) = Ok a Nothing+eitherToResult (Left e) = ThrowHaxl e Nothing+++-- -----------------------------------------------------------------------------+-- 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 w+ = forall a . CompleteReq+ (ResultVal a w)+ !(IVar u w 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.++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 w a+ = Done a+ | Throw SomeException+ | forall b . Blocked+ {-# UNPACK #-} !(IVar u w b)+ (Cont u w 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 w a@, but see+ -- 'IVar',++instance (Show a) => Show (Result u w a) where+ show (Done a) = printf "Done(%s)" $ show a+ show (Throw e) = printf "Throw(%s)" $ show e+ show Blocked{} = "Blocked"++instance Functor (Result u w) where+ fmap f (Done a) = Done (f a)+ fmap _ (Throw exc) = Throw exc+ fmap f (Blocked ivar cont) = Blocked ivar (f :<$> cont)++{- 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 w a+ = Cont (GenHaxl u w a)+ | forall b. Cont u w b :>>= (b -> GenHaxl u w a)+ | forall b. (b -> a) :<$> (Cont u w b)+ | Return (IVar u w a)++toHaxl :: Cont u w a -> GenHaxl u w a+toHaxl (Cont haxl) = haxl+toHaxl (m :>>= k) = toHaxlBind m k+toHaxl (f :<$> x) = toHaxlFmap f x+toHaxl (Return i) = getIVar i++toHaxlBind :: Cont u w b -> (b -> GenHaxl u w a) -> GenHaxl u w a+toHaxlBind (m :>>= k) k2 = toHaxlBind m (k >=> k2)+toHaxlBind (Cont haxl) k = haxl >>= k+toHaxlBind (f :<$> x) k = toHaxlBind x (k . f)+toHaxlBind (Return i) k = getIVar i >>= k++toHaxlFmap :: (a -> b) -> Cont u w a -> GenHaxl u w b+toHaxlFmap f (m :>>= k) = toHaxlBind m (k >=> return . f)+toHaxlFmap f (Cont haxl) = f <$> haxl+toHaxlFmap f (g :<$> x) = toHaxlFmap (f . g) x+toHaxlFmap f (Return i) = f <$> getIVar i++-- -----------------------------------------------------------------------------+-- Monad/Applicative instances++instance Monad (GenHaxl u w) where+ return = pure+ 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))++ -- We really want the Applicative version of >>+ (>>) = (*>)++#if __GLASGOW_HASKELL__ >= 808+instance CTL.MonadFail (GenHaxl u w) where+#endif+ fail msg = GenHaxl $ \_env ->+ return $ Throw $ toException $ MonadFail $ Text.pack msg++instance Functor (GenHaxl u w) 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 w) where+ pure a = GenHaxl $ \_env -> pure (Done a)+ 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" $+ blockedBlocked env ivar1 fcont ivar2 acont+ -- Note [Blocked/Blocked]++instance Semigroup a => Semigroup (GenHaxl u w a) where+ (<>) = liftA2 (<>)++instance Monoid a => Monoid (GenHaxl u w a) where+ mempty = pure mempty+#if __GLASGOW_HASKELL__ < 804+ mappend = liftA2 mappend+#endif++blockedBlocked+ :: Env u w+ -> IVar u w c+ -> Cont u w (a -> b)+ -> IVar u w d+ -> Cont u w a+ -> IO (Result u w b)+blockedBlocked _ _ (Return i) ivar2 acont =+ return (Blocked ivar2 (acont :>>= getIVarApply i))+blockedBlocked _ _ (g :<$> Return i) ivar2 acont =+ return (Blocked ivar2 (acont :>>= \ a -> (\f -> g f a) <$> getIVar i))+blockedBlocked env ivar1 fcont ivar2 acont = 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 w -> a) -> GenHaxl u w 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 w -> GenHaxl u w a -> GenHaxl u w 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))))++nextCallId :: Env u w -> IO CallId+nextCallId env = atomicModifyIORef' (callIdRef env) $ \x -> (x+1,x+1)++-- | Runs the Haxl computation, transforming the writes within using the+-- provided function.+--+-- Memoization behavior is unchanged, meaning if a memoized computation is run+-- once inside @mapWrites@ and then once without, the writes from the second run+-- will NOT be transformed.+mapWrites :: Monoid w => (w -> w) -> GenHaxl u w a -> GenHaxl u w a+mapWrites f action = GenHaxl $ \curEnv -> do+ wlogs <- newIORef mempty+ wlogsNoMemo <- newIORef mempty+ let+ !newEnv = curEnv { writeLogsRef = wlogs, writeLogsRefNoMemo = wlogsNoMemo }+ unHaxl (mapWritesImpl curEnv newEnv action) newEnv+ where+ mapWritesImpl oldEnv curEnv (GenHaxl m) = GenHaxl $ \_ -> do+ let+ pushTransformedWrites = do+ wt <- readIORef $ writeLogsRef curEnv+ modifyIORef' (writeLogsRef oldEnv) (<> f wt)+ wtNoMemo <- readIORef $ writeLogsRefNoMemo curEnv+ modifyIORef' (writeLogsRefNoMemo oldEnv) (<> f wtNoMemo)++ r <- m curEnv++ case r of+ Done a -> pushTransformedWrites >> return (Done a)+ Throw e -> pushTransformedWrites >> return (Throw e)+ Blocked ivar k -> return+ (Blocked ivar (Cont (mapWritesImpl oldEnv curEnv (toHaxl k))))++#ifdef PROFILING+-- -----------------------------------------------------------------------------+-- CallGraph recording++-- | Returns a version of the Haxl computation which records function calls in+-- an edge list which is the function call graph. Each function that is to be+-- recorded must be wrapped with a call to @withCallGraph@.+withCallGraph+ :: Typeable a+ => (a -> Maybe Text)+ -> QualFunction+ -> GenHaxl u w a+ -> GenHaxl u w a+withCallGraph toText f a = do+ coreEnv <- env id+ -- TODO: Handle exceptions+ value <- withEnv coreEnv{currFunction = f} a+ case callGraphRef coreEnv of+ Just graph -> unsafeLiftIO $ modifyIORef' graph+ (updateCallGraph (f, currFunction coreEnv) (toText value))+ _ -> throw $ CriticalError+ "withCallGraph called without an IORef CallGraph"+ return value+ where+ updateCallGraph :: FunctionCall -> Maybe Text -> CallGraph -> CallGraph+ updateCallGraph fnCall@(childQFunc, _) (Just value) (edgeList, valueMap) =+ (fnCall : edgeList, Map.insert childQFunc value valueMap)+ updateCallGraph fnCall Nothing (edgeList, valueMap) =+ (fnCall : edgeList, valueMap)+#endif++-- -----------------------------------------------------------------------------+-- Exceptions++-- | Throw an exception in the Haxl monad+throw :: Exception e => e -> GenHaxl u w a+throw e = GenHaxl $ \env -> raise env e++raise :: Exception e => Env u w -> e -> IO (Result u w a)+raise env e = raiseImpl env (toException e)+#ifdef PROFILING+ currentCallStack+#endif+++raiseFromIVar :: Exception e => Env u w -> IVar u w a -> e -> IO (Result u w b)+#ifdef PROFILING+raiseFromIVar env IVar{..} e =+ raiseImpl env (toException e) (ccsToStrings ivarCCS)+#else+raiseFromIVar env _ivar e = raiseImpl env (toException e)+#endif++{-# INLINE raiseImpl #-}+#ifdef PROFILING+raiseImpl :: Env u w -> SomeException -> IO [String] -> IO (Result u w b)+raiseImpl Env{..} e getCostCentreStack+#else+raiseImpl :: Env u w -> SomeException -> IO (Result u w b)+raiseImpl Env{..} e+#endif+ | testReportFlag ReportExceptionLabelStack $ report flags+ , Just (HaxlException Nothing h) <- fromException e = do+ let stk = NonEmpty.toList $ profLabelStack profCurrent+ return $ Throw $ toException $ HaxlException (Just stk) h+#ifdef PROFILING+ | Just (HaxlException Nothing h) <- fromException e = do+ stk <- reverse . map Text.pack <$> getCostCentreStack+ return $ Throw $ toException $ HaxlException (Just stk) h+#endif+ | otherwise = return $ Throw e++-- | Catch an exception in the Haxl monad+catch :: Exception e => GenHaxl u w a -> (e -> GenHaxl u w a) -> GenHaxl u w 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 w a -> (e -> GenHaxl u w a)+ -> GenHaxl u w 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 w a -> GenHaxl u w (Either e a)+try haxl = (Right <$> haxl) `catch` (return . Left)++-- | @since 0.3.1.0+instance Catch.MonadThrow (GenHaxl u w) where throwM = Haxl.Core.Monad.throw+-- | @since 0.3.1.0+instance Catch.MonadCatch (GenHaxl u w) 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 w 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.+--+-- Note: this function does not catch async exceptions. This is a flaw in Haxl+-- where it can sometimes leave the environment in a bad state when async+-- exceptions are thrown (for example the cache may think a fetch is happening+-- but the exception has stopped it). TODO would be to make Haxl async exception+-- safe and then remove the rethrowAsyncExceptions below, but for now this is+-- safer to avoid bugs. Additionally this would not protect you from async+-- exceptions thrown while executing code in the scheduler, and so relying on+-- this function to catch all async exceptions would be ambitious at best.+unsafeToHaxlException :: GenHaxl u w a -> GenHaxl u w a+unsafeToHaxlException (GenHaxl m) = GenHaxl $ \env -> do+ r <- m env `Exception.catch` \e -> do+ rethrowAsyncExceptions 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 w a -> GenHaxl u w (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 w ()+-- > loadCache = do+-- > cacheRequest (ListWombats 3) (Right ([1,2,3]))+-- > cacheRequest (CountAardvarks "abcabc") (Right (2))+--+dumpCacheAsHaskell :: GenHaxl u w String+dumpCacheAsHaskell =+ dumpCacheAsHaskellFn "loadCache" "GenHaxl u w ()" "cacheRequest"++-- | Dump the contents of the cache as Haskell code that, when+-- compiled and run, will recreate the same cache contents.+-- Does not take into account the writes done as part of the computation.+--+-- Takes the name and type for the resulting function as arguments.+-- Also takes the cacheFn to use, we can use either @cacheRequest@ or+-- @dupableCacheRequest@.+dumpCacheAsHaskellFn :: String -> String -> String -> GenHaxl u w String+dumpCacheAsHaskellFn fnName fnType cacheFn = do+ cache <- env dataCache -- NB. dataCache, not memoCache. We ignore memoized+ -- results when dumping the cache.+ let+ readIVar (DataCacheItem IVar{ivarRef = !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 cacheFn <+> 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+ showCache cache readIVar++ let+ body = if null entries+ then text "return ()"+ else vcat (map mk_cr (concatMap snd entries))++ return $ show $+ text (fnName ++ " :: " ++ fnType) $$+ text (fnName ++ " = do") $$+ nest 2 body $$ text "" -- final newline
+ Haxl/Core/Parallel.hs view
@@ -0,0 +1,153 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | Psuedo-parallel operations. Most users should import "Haxl.Core"+-- instead.+--+module Haxl.Core.Parallel+ ( -- * Parallel operations+ biselect+ , pAnd+ , pOr+ , unsafeChooseFirst+ ) where++import Haxl.Core.Monad hiding (catch, throw)+import Haxl.Core.Exception++import Control.Exception (throw)++-- -----------------------------------------------------------------------------+-- Parallel operations++-- Bind more tightly than .&&, .||+infixr 5 `pAnd`+infixr 4 `pOr`+++biselect :: GenHaxl u w (Either a b)+ -> GenHaxl u w (Either a c)+ -> GenHaxl u w (Either a (b,c))+biselect haxla haxlb = biselect_opt id id Left Right haxla haxlb++{-# INLINE biselect_opt #-}+biselect_opt :: (l -> Either a b)+ -> (r -> Either a c)+ -> (a -> t)+ -> ((b,c) -> t)+ -> GenHaxl u w l+ -> GenHaxl u w r+ -> GenHaxl u w t+biselect_opt discrimA discrimB left right haxla haxlb =+ let go (GenHaxl haxla) (GenHaxl haxlb) = GenHaxl $ \env -> do+ ra <- haxla env+ case ra of+ Done ea ->+ case discrimA ea of+ Left a -> return (Done (left a))+ Right b -> do+ rb <- haxlb env+ case rb of+ Done eb ->+ case discrimB eb of+ Left a -> return (Done (left a))+ Right c -> return (Done (right (b,c)))+ Throw e -> return (Throw e)+ Blocked ib haxlb' ->+ return (Blocked ib+ (haxlb' :>>= \b' -> go_right b b'))+ Throw e -> return (Throw e)+ Blocked ia haxla' -> do+ rb <- haxlb env+ case rb of+ Done eb ->+ case discrimB eb of+ Left a -> return (Done (left a))+ Right c ->+ return (Blocked ia+ (haxla' :>>= \a' -> go_left a' c))+ Throw e -> return (Throw e)+ Blocked ib haxlb' -> do+ i <- newIVar+ addJob env (return ()) i ia+ addJob env (return ()) i ib+ return (Blocked i (Cont (go (toHaxl haxla') (toHaxl haxlb'))))+ -- The code above makes sure that the computation+ -- wakes up whenever either 'ia' or 'ib' is filled.+ -- The ivar 'i' is used as a synchronisation point+ -- for the whole computation, and we make sure that+ -- whenever 'ia' or 'ib' are filled in then 'i' will+ -- also be filled.++ go_right b eb =+ case discrimB eb of+ Left a -> return (left a)+ Right c -> return (right (b,c))+ go_left ea c =+ case discrimA ea of+ Left a -> return (left a)+ Right b -> return (right (b,c))+ in go haxla haxlb++-- | 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 w Bool -> GenHaxl u w Bool -> GenHaxl u w Bool+pOr x y = biselect_opt discrim discrim left right x y+ where+ discrim True = Left ()+ discrim False = Right ()+ left _ = True+ right _ = False++-- | 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 w Bool -> GenHaxl u w Bool -> GenHaxl u w Bool+pAnd x y = biselect_opt discrim discrim left right x y+ where+ discrim False = Left ()+ discrim True = Right ()+ left _ = False+ right _ = True++-- | This function takes two haxl computations as input, and returns the+-- output of whichever computation finished first. This is clearly+-- non-deterministic in its output and exception behavior, be careful when+-- using it.+unsafeChooseFirst+ :: GenHaxl u w a+ -> GenHaxl u w b+ -> GenHaxl u w (Either a b)+unsafeChooseFirst x y = biselect_opt discrimx discrimy id right x y+ where+ discrimx :: a -> Either (Either a b) ()+ discrimx a = Left (Left a)++ discrimy :: b -> Either (Either a b) ()+ discrimy b = Left (Right b)++ right _ = throw $ CriticalError+ "unsafeChooseFirst: We should never have a 'Right ()'"
+ Haxl/Core/Profile.hs view
@@ -0,0 +1,244 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}++-- | 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.List.NonEmpty (NonEmpty(..), (<|))+#if __GLASGOW_HASKELL__ < 804+import Data.Monoid+#endif+import Data.Typeable+import qualified Data.HashMap.Strict as HashMap+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 w a -> GenHaxl u w a+withLabel l (GenHaxl m) = GenHaxl $ \env ->+ if not $ testReportFlag ReportProfiling $ report $ flags env+ 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 w a -> GenHaxl u w a+withFingerprintLabel mnPtr nPtr (GenHaxl m) = GenHaxl $ \env ->+ if not $ testReportFlag ReportProfiling $ report $ flags env+ 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 w -> IO (Result u w a))+ -> Env u w+ -> IO (Result u w a)+collectProfileData l m env = do+ let ProfileCurrent prevProfKey (prevProfLabel :| _) = profCurrent env+ if prevProfLabel == l+ then+ -- do not add a new label if we are recursing+ m env+ else do+ key <- atomicModifyIORef' (profRef env) $ \p ->+ case HashMap.lookup (l, prevProfKey) (profileTree p) of+ Just k -> (p, k)+ Nothing -> (p+ { profileTree = HashMap.insert+ (l, prevProfKey)+ (profileNextKey p)+ (profileTree p)+ , profileNextKey = profileNextKey p + 1 }, profileNextKey p)+ runProfileData l key m False env+{-# INLINE collectProfileData #-}++runProfileData+ :: ProfileLabel+ -> ProfileKey+ -> (Env u w -> IO (Result u w a))+ -> Bool+ -> Env u w+ -> IO (Result u w a)+runProfileData l key m isCont env = do+ t0 <- getTimestamp+ a0 <- getAllocationCounter+ let+ ProfileCurrent caller stack = profCurrent env+ nextCurrent = ProfileCurrent+ { profCurrentKey = key+ , profLabelStack = l <| stack+ }+ runCont (GenHaxl h) = GenHaxl $ runProfileData l key h True++ r <- m env{profCurrent=nextCurrent} -- what if it throws?++ -- Make the result strict in Done/Throw so that if the user code+ -- returns (force a), the force is evaluated *inside* the profile.+ result <- case r of+ Done !a -> return (Done a)+ Throw !e -> return (Throw e)+ Blocked ivar k -> return (Blocked ivar (Cont $ runCont (toHaxl k)))++ a1 <- getAllocationCounter+ t1 <- getTimestamp++ -- caller might not be the actual caller of this function+ -- for example MAIN may be continuing a function from the middle of the stack.+ -- But this is what we want as we need to account for allocations.+ -- So do not be tempted to pass through prevProfKey (from collectProfileData)+ -- which is the original caller+ modifyProfileData env key caller (a0 - a1) (t1-t0) (if isCont then 0 else 1)++ -- So we do not count the allocation overhead of modifyProfileData+ setAllocationCounter a1+ return result+{-# INLINE runProfileData #-}++modifyProfileData+ :: Env u w+ -> ProfileKey+ -> ProfileKey+ -> AllocCount+ -> Microseconds+ -> LabelHitCount+ -> IO ()+modifyProfileData env key caller allocs t labelIncrement = do+ modifyIORef' (profRef env) $ \ p ->+ p { profile =+ HashMap.insertWith updEntry key newEntry .+ HashMap.insertWith updCaller caller newCaller $+ profile p }+ where newEntry =+ emptyProfileData+ { profileAllocs = allocs+ , profileLabelHits = labelIncrement+ , profileTime = t+ }+ updEntry _ old =+ old+ { profileAllocs = profileAllocs old + allocs+ , profileLabelHits = profileLabelHits old + labelIncrement+ , profileTime = profileTime old + t+ }+ -- subtract allocs/time 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+ , profileTime = -t+ }+ updCaller _ old =+ old { profileAllocs = profileAllocs old - allocs+ , profileTime = profileTime old - t+ }+++-- 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 w -> IO (Result u w a))+ -> Env u w+ -> IO (Result u w a)+profileCont m env = do+ t0 <- getTimestamp+ a0 <- getAllocationCounter+ r <- m env+ a1 <- getAllocationCounter+ t1 <- getTimestamp+ let+ allocs = a0 - a1+ t = t1 - t0+ newEntry = emptyProfileData+ { profileAllocs = allocs+ , profileTime = t+ }+ updEntry _ old = old+ { profileAllocs = profileAllocs old + allocs+ , profileTime = profileTime old + t+ }+ profKey = profCurrentKey (profCurrent env)+ modifyIORef' (profRef env) $ \ p ->+ p { profile =+ HashMap.insertWith updEntry profKey newEntry $+ profile p }+ -- So we do not count the allocation overhead of modifyProfileData+ setAllocationCounter a1+ return r+{-# INLINE profileCont #-}++incrementMemoHitCounterFor :: Env u w -> CallId -> Bool -> IO ()+incrementMemoHitCounterFor env callId wasCached = do+ modifyIORef' (profRef env) $ \p -> p {+ profile = HashMap.insertWith+ upd+ (profCurrentKey $ profCurrent env)+ (emptyProfileData { profileMemos = [val] })+ (profile p)+ }+ where+ val = ProfileMemo callId wasCached+ upd _ old = old { profileMemos = val : profileMemos old }++{-# NOINLINE addProfileFetch #-}+addProfileFetch+ :: forall r u w a . (DataSourceName r, Eq (r a), Hashable (r a), Typeable (r a))+ => Env u w -> r a -> CallId -> Bool -> IO ()+addProfileFetch env _req cid wasCached = do+ c <- getAllocationCounter+ let (ProfileCurrent profKey _) = profCurrent env+ modifyIORef' (profRef env) $ \ p ->+ let+ val = ProfileFetch cid (memoKey env) wasCached+ upd _ old = old { profileFetches = val : profileFetches old }++ in p { profile =+ HashMap.insertWith+ upd+ profKey+ (emptyProfileData { profileFetches = [val] })+ (profile p)+ }+ -- So we do not count the allocation overhead of addProfileFetch+ setAllocationCounter c
Haxl/Core/RequestStore.hs view
@@ -1,14 +1,16 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE BangPatterns #-} -- | Bucketing requests by 'DataSource'. -- -- When a request is issued by the client via 'dataFetch', it is placed@@ -16,14 +18,35 @@ -- of requests, the 'contents' operation extracts the fetches, bucketed -- by 'DataSource'. ---module Haxl.Core.RequestStore (- BlockedFetches(..), RequestStore,- noRequests, addRequest, contents+-- This module is provided for access to Haxl internals only; most+-- users should not need to import it.+--+module Haxl.Core.RequestStore+ ( BlockedFetches(..)+ , BlockedFetchInternal(..)+ , RequestStore+ , isEmpty+ , noRequests+ , addRequest+ , contents+ , getSize+ , ReqCountMap(..)+ , emptyReqCounts+ , filterRCMap+ , getMapFromRCMap+ , getSummaryMapFromRCMap+ , addToCountMap+ , subFromCountMap ) where -import Haxl.Core.Types+import Haxl.Core.DataSource+import Haxl.Core.Stats import Data.Map (Map)+import qualified Data.HashMap.Strict as HashMap import qualified Data.Map.Strict as Map+import Data.Proxy+import Data.Text (Text)+import Data.Kind (Type) import Data.Typeable import Unsafe.Coerce @@ -33,10 +56,16 @@ -- is dynamically-typed. It maps the TypeRep of the request to the -- 'BlockedFetches' for that 'DataSource'. +newtype BlockedFetchInternal = BlockedFetchInternal CallId+ -- | A batch of 'BlockedFetch' objects for a single 'DataSource' data BlockedFetches u =- forall r. (DataSource u r) => BlockedFetches [BlockedFetch r]+ forall r. (DataSource u r) =>+ BlockedFetches [BlockedFetch r] [BlockedFetchInternal] +isEmpty :: RequestStore u -> Bool+isEmpty (RequestStore m) = Map.null m+ -- | A new empty 'RequestStore'. noRequests :: RequestStore u noRequests = RequestStore Map.empty@@ -44,13 +73,13 @@ -- | Adds a 'BlockedFetch' to a 'RequestStore'. addRequest :: forall u r. (DataSource u r)- => BlockedFetch r -> RequestStore u -> RequestStore u-addRequest bf (RequestStore m) =- RequestStore $ Map.insertWith combine ty (BlockedFetches [bf]) m+ => BlockedFetch r -> BlockedFetchInternal -> RequestStore u -> RequestStore u+addRequest bf bfi (RequestStore m) =+ RequestStore $ Map.insertWith combine ty (BlockedFetches [bf] [bfi]) m where combine :: BlockedFetches u -> BlockedFetches u -> BlockedFetches u- combine _ (BlockedFetches bfs)- | typeOf1 (getR bfs) == ty = BlockedFetches (unsafeCoerce bf:bfs)+ combine _ (BlockedFetches bfs bfis)+ | typeOf1 (getR bfs) == ty = BlockedFetches (unsafeCoerce bf:bfs) (bfi:bfis) | otherwise = error "RequestStore.insert" -- the dynamic type check here should be unnecessary, but if -- there are bugs in `Typeable` or `Map` then we'll get an@@ -63,8 +92,79 @@ -- The TypeRep of requests for this data source ty :: TypeRep- ty = typeOf1 (undefined :: r a)+ !ty = typeOf1 (undefined :: r a) -- | Retrieves the whole contents of the 'RequestStore'. contents :: RequestStore u -> [BlockedFetches u] contents (RequestStore m) = Map.elems m++getSize :: RequestStore u -> Int+getSize (RequestStore m) = Map.size m++-- A counter to keep track of outgone requests. Entries are added to this+-- map as we send requests to datasources, and removed as these fetches+-- are completed.+-- This is a 2 level map: the 1st level stores requests for a particular+-- datasource, the 2nd level stores count of requests per type.+newtype ReqCountMap = ReqCountMap (Map Text (Map TypeRep Int))+ deriving (Show)++emptyReqCounts :: ReqCountMap+emptyReqCounts = ReqCountMap Map.empty++addToCountMap+ :: forall (r :: Type -> Type). (DataSourceName r, Typeable r)+ => Proxy r+ -> Int -- type and number of requests+ -> ReqCountMap+ -> ReqCountMap+addToCountMap = updateCountMap (+)++subFromCountMap+ :: forall (r :: Type -> Type). (DataSourceName r, Typeable r)+ => Proxy r+ -> Int -- type and number of requests+ -> ReqCountMap+ -> ReqCountMap+subFromCountMap = updateCountMap (-)++updateCountMap+ :: forall (r :: Type -> Type). (DataSourceName r, Typeable r)+ => (Int -> Int -> Int)+ -> Proxy r+ -> Int -- type and number of requests+ -> ReqCountMap+ -> ReqCountMap+updateCountMap op p n (ReqCountMap m) = ReqCountMap $ Map.insertWith+ (flip (Map.unionWith op)) -- flip is important as "op" is not commutative+ (dataSourceName p) (Map.singleton ty n)+ m+ where+ -- The TypeRep of requests for this data source+ -- The way this is implemented, all elements in the 2nd level map will be+ -- mapped to the same key, as all requests to a datasource have the same+ -- "type". It will be more beneficial to be able to instead map requests+ -- to their names (ie, data constructor) - but there's no cheap way of doing+ -- that.+ ty :: TypeRep+ !ty = typeOf1 (undefined :: r a)++-- Filter all keys with 0 fetches. Since ReqCountMap is a 2-level map, we need+-- nested filter operations.+filterRCMap :: ReqCountMap -> ReqCountMap+filterRCMap (ReqCountMap m) = ReqCountMap $+ Map.filter ((> 0) . Map.size) (Map.filter (> 0) <$> m)++-- Filters the ReqCountMap by default+getMapFromRCMap :: ReqCountMap -> Map Text (Map TypeRep Int)+getMapFromRCMap r+ | ReqCountMap m <- filterRCMap r = m++getSummaryMapFromRCMap :: ReqCountMap -> HashMap.HashMap Text Int+getSummaryMapFromRCMap (ReqCountMap m) = HashMap.fromList+ [ (k, s)+ | (k, v) <- Map.toList m+ , not $ Map.null v+ , let s = sum $ Map.elems v+ , s > 0+ ]
+ Haxl/Core/Run.hs view
@@ -0,0 +1,290 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE DeriveDataTypeable #-}++-- | Defines 'runHaxl'. Most users should import "Haxl.Core" instead.+--+module Haxl.Core.Run+ ( runHaxl+ , runHaxlWithWrites+ ) where++import Control.Concurrent.STM+import Control.Exception as Exception+import Control.Monad+import Data.IORef+import Data.Maybe+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+import Haxl.Core.Util++import qualified Data.HashTable.IO as H++-- -----------------------------------------------------------------------------+-- runHaxl++-- | Runs a 'Haxl' computation in the given 'Env'.+--+-- Note: to make multiple concurrent calls to 'runHaxl', each one must+-- have a separate 'Env'. A single 'Env' must /not/ be shared between+-- multiple concurrent calls to 'runHaxl', otherwise deadlocks or worse+-- will likely ensue.+--+-- However, multiple 'Env's may share a single 'StateStore', and thereby+-- use the same set of datasources.+runHaxl:: forall u w a. Monoid w => Env u w -> GenHaxl u w a -> IO a+runHaxl env haxl = fst <$> runHaxlWithWrites env haxl++runHaxlWithWrites :: forall u w a. Monoid w => Env u w -> GenHaxl u w a -> IO (a, w)+runHaxlWithWrites env@Env{..} haxl = do+ result@IVar{ivarRef = resultRef} <- newIVar -- where to put the final result+ ifTraceLog <- do+ if trace flags < 3+ then return $ \_ -> return ()+ else do+ start <- getTimestamp+ return $ \s -> do+ now <- getTimestamp+ let t = fromIntegral (now - start) / 1000.0 :: Double+ printf "%.1fms: %s" t (s :: String)+ let+ -- Run a job, and put its result in the given IVar+ schedule :: Env u w -> JobList u w -> GenHaxl u w b -> IVar u w b -> IO ()+ schedule env@Env{..} rq (GenHaxl run) ivar@IVar{ivarRef = !ref} = do+ ifTraceLog $ printf "schedule: %d\n" (1 + lengthJobList rq)+ let {-# INLINE result #-}+ result r = do+ e <- readIORef ref+ case e of+ IVarFull _ ->+ -- An IVar is typically only meant to be written to once+ -- so it would make sense to throw an error here. But there+ -- are legitimate use-cases for writing several times.+ -- (See Haxl.Core.Parallel)+ reschedule env rq+ 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 testReportFlag ReportProfiling $ report flags -- 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) -> do+ wt <- readIORef writeLogsRef+ result $ Ok a (Just wt)+ Right (Throw ex) -> do+ wt <- readIORef writeLogsRef+ result $ ThrowHaxl ex (Just wt)+ Right (Blocked i fn) -> do+ addJob env (toHaxl fn) ivar i+ 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 w -> JobList u w -> 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 w -> IO ()+ emptyRunQueue env = do+ ifTraceLog $ printf "emptyRunQueue\n"+ haxls <- checkCompletions env+ case haxls of+ JobNil -> checkRequestStore env+ _ -> reschedule env haxls++ checkRequestStore :: Env u w -> IO ()+ checkRequestStore env@Env{..} = do+ ifTraceLog $ printf "checkRequestStore\n"+ reqStore <- readIORef reqStoreRef+ if RequestStore.isEmpty reqStore+ then waitCompletions env+ else do+ ifTraceLog $ printf "performFetches %d\n" (RequestStore.getSize reqStore)+ writeIORef reqStoreRef noRequests+ performRequestStore env reqStore+ -- 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) $ do+ let DataCache dc = dataCache+ H.foldM (\_ (k, _) -> H.delete dc k) () dc+ emptyRunQueue env++ checkCompletions :: Env u w -> IO (JobList u w)+ checkCompletions Env{..} = do+ ifTraceLog $ printf "checkCompletions\n"+ comps <- atomicallyOnBlocking (LogicBug ReadingCompletionsFailedRun) $ do+ c <- readTVar completions+ writeTVar completions []+ return c+ case comps of+ [] -> return JobNil+ _ -> do+ ifTraceLog $ printf "%d complete\n" (length comps)+ let+ getComplete (CompleteReq a IVar{ivarRef = !cr} allocs) = do+ when (allocs < 0) $ do+ cur <- getAllocationCounter+ setAllocationCounter (cur + allocs)+ r <- readIORef cr+ case r of+ IVarFull _ -> do+ ifTraceLog $ 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 a)+ return cv+ jobs <- mapM getComplete comps+ return (foldr appendJobList JobNil jobs)++ waitCompletions :: Env u w -> IO ()+ waitCompletions env@Env{..} = do+ ifTraceLog $ printf "waitCompletions\n"+ let+ wrapped = atomicallyOnBlocking (LogicBug ReadingCompletionsFailedRun)+ doWait = wrapped $ do+ c <- readTVar completions+ when (null c) retry+ doWaitProfiled = do+ queueEmpty <- null <$> wrapped (readTVar completions)+ when queueEmpty $ do+ -- Double check the queue as we want to make sure that+ -- submittedReqsRef is copied before waiting on the queue but as a+ -- fast path do not want to copy it if the queue is empty.+ -- There is still a race oppoortunity as submittedReqsRef is+ -- decremented in whatever thread the completion happens, and so it+ -- is possible for waitingOn to be empty while queueEmpty2 is True.+ waitingOn <- readIORef submittedReqsRef+ queueEmpty2 <- null <$> wrapped (readTVar completions)+ when queueEmpty2 $ do+ start <- getTimestamp+ doWait+ end <- getTimestamp+ let fw = FetchWait+ { fetchWaitReqs = getSummaryMapFromRCMap waitingOn+ , fetchWaitStart = start+ , fetchWaitDuration = (end-start)+ }+ modifyIORef' statsRef $ \(Stats s) -> Stats (fw:s)+ if testReportFlag ReportFetchStats $ report flags+ then doWaitProfiled+ else doWait+ emptyRunQueue env++ --+ schedule env JobNil haxl result+ r <- readIORef resultRef+ writeIORef writeLogsRef mempty+ wtNoMemo <- atomicModifyIORef' writeLogsRefNoMemo+ (\old_wrts -> (mempty, old_wrts))+ case r of+ IVarEmpty _ -> throwIO (CriticalError "runHaxl: missing result")+ IVarFull (Ok a wt) -> do+ return (a, fromMaybe mempty wt <> wtNoMemo)+ IVarFull (ThrowHaxl e _wt) -> throwIO e+ -- The written logs are discarded when there's a Haxl exception. We+ -- can change this behavior if we need to get access to partial logs.+ 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 computation+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+-}++-- | An exception thrown when reading from datasources fails+data ReadingCompletionsFailedRun = ReadingCompletionsFailedRun+ deriving Show++instance Exception ReadingCompletionsFailedRun
− Haxl/Core/Show1.hs
@@ -1,15 +0,0 @@--- Copyright (c) 2014, 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.--module Haxl.Core.Show1- ( Show1(..)- ) where---- | A class of type constructors for which we can show all--- parameterizations.-class Show1 f where- show1 :: f a -> String
+ Haxl/Core/ShowP.hs view
@@ -0,0 +1,20 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++-- |+-- Most users should import "Haxl.Core" instead of importing this+-- module directly.+--+module Haxl.Core.ShowP+ ( ShowP(..)+ ) where++-- | A class of type constructors for which we can show all+-- parameterizations.+class ShowP f where+ showp :: f a -> String
Haxl/Core/StateStore.hs view
@@ -1,22 +1,35 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE CPP #-} -module Haxl.Core.StateStore (- StateKey(..), StateStore, stateGet, stateSet, stateEmpty+-- |+-- Most users should import "Haxl.Core" instead of importing this+-- module directly.+--+module Haxl.Core.StateStore+ ( StateKey(..)+ , StateStore+ , stateGet+ , stateSet+ , stateEmpty ) where import Data.Map (Map)+import Data.Kind (Type) import qualified Data.Map.Strict as Map+#if __GLASGOW_HASKELL__ < 804+import Data.Monoid+#endif import Data.Typeable import Unsafe.Coerce @@ -24,17 +37,30 @@ -- instance of 'StateKey' can store and retrieve information from a -- 'StateStore'. ---#if __GLASGOW_HASKELL__ >= 708-class Typeable f => StateKey (f :: * -> *) where- data State f-#else-class Typeable1 f => StateKey (f :: * -> *) where+class Typeable f => StateKey (f :: Type -> Type) where data State f-#endif + -- | 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) +#if __GLASGOW_HASKELL__ >= 804+instance Semigroup StateStore where+ -- Left-biased union+ StateStore m1 <> StateStore m2 = StateStore $ m1 <> m2+#endif++instance Monoid StateStore where+ mempty = stateEmpty+#if __GLASGOW_HASKELL__ < 804+ mappend (StateStore m1) (StateStore m2) = StateStore $ m1 <> m2+#endif+ -- | Encapsulates the type of 'StateStore' data so we can have a -- heterogeneous collection. data StateStoreData = forall f. StateKey f => StateStoreData (State f)@@ -46,22 +72,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,313 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE CPP #-}++-- |+-- 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(..)+ , CallId+ , FetchStats(..)+ , Microseconds+ , Timestamp+ , DataSourceStats(..)+ , getTimestamp+ , emptyStats+ , numFetches+ , ppStats+ , ppFetchStats+ , aggregateFetchBatches++ -- * Profiling+ , Profile(..)+ , ProfileMemo(..)+ , ProfileFetch(..)+ , emptyProfile+ , ProfileKey+ , ProfileLabel+ , ProfileData(..)+ , emptyProfileData+ , AllocCount+ , LabelHitCount++ -- * Allocation+ , getAllocationCounter+ , setAllocationCounter+ ) where++import Data.Aeson+import Data.Function (on)+import Data.Maybe (mapMaybe)+import Data.HashMap.Strict (HashMap)+import Data.Int+import Data.List (intercalate, sortOn, groupBy)+#if __GLASGOW_HASKELL__ < 804+import Data.Semigroup (Semigroup)+#endif+import Data.Ord (Down(..))+import Data.Text (Text)+import Data.Time.Clock.POSIX+import Data.Typeable+import Text.Printf+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text as Text++import GHC.Conc (getAllocationCounter, setAllocationCounter)++-- ---------------------------------------------------------------------------+-- 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++data DataSourceStats =+ forall a. (Typeable a, Show a, Eq a, ToJSON a) => DataSourceStats a++instance Show DataSourceStats where+ show (DataSourceStats x) = printf "DataSourceStats %s" (show x)++instance Eq DataSourceStats where+ (==) (DataSourceStats a) (DataSourceStats b) =+ cast a == Just b++-- | 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 fetchSymbol rs+ else '-'+ | t <- [1..numDashes]+ ]+ ++ "] " ++ show i ++ " - " ++ ppFetchStats rs+ | (i, rs) <- zip [(1::Int)..] validFetchStats ]+ where+ isFetchStats FetchStats{} = True+ isFetchStats FetchWait{} = True+ isFetchStats FetchDataSourceStats{} = True+ isFetchStats _ = False+ validFetchStats = filter isFetchStats (reverse rss)+ numDashes = 50+ getStart FetchStats{..} = Just fetchStart+ getStart FetchWait{..} = Just fetchWaitStart+ getStart _ = Nothing+ getEnd FetchStats{..} = Just $ fetchStart + fetchDuration+ getEnd FetchWait{..} = Just $ fetchWaitStart + fetchWaitDuration+ getEnd _ = Nothing+ minStartTime = minimum $ mapMaybe getStart validFetchStats+ endTime = maximum $ mapMaybe getEnd validFetchStats+ usPerDash = (endTime - minStartTime) `div` numDashes+ fetchSymbol FetchStats{} = '*'+ fetchSymbol FetchWait{} = '.'+ fetchSymbol _ = '?'+ fetchWasRunning :: FetchStats -> Timestamp -> Timestamp -> Bool+ fetchWasRunning fs@FetchStats{} t1 t2 =+ (fetchStart fs + fetchDuration fs) >= t1 && fetchStart fs < t2+ fetchWasRunning fw@FetchWait{} t1 t2 =+ (fetchWaitStart fw + fetchWaitDuration fw) >= t1 && fetchWaitStart fw < t2+ fetchWasRunning _ _ _ = False++type CallId = Int++-- | 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 :: {-# UNPACK #-} !Timestamp+ , fetchDuration :: {-# UNPACK #-} !Microseconds+ , fetchSpace :: {-# UNPACK #-} !Int64+ , fetchFailures :: {-# UNPACK #-} !Int+ , fetchIgnoredFailures :: {-# UNPACK #-} !Int+ , fetchBatchId :: {-# UNPACK #-} !Int+ , fetchIds :: [CallId]+ }++ -- | 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]+ , fetchStatId :: {-# UNPACK #-} !CallId+ }+ | MemoCall+ { memoStatId :: {-# UNPACK #-} !CallId+ , memoSpace :: {-# UNPACK #-} !Int64+ }+ | FetchWait+ { fetchWaitReqs :: HashMap Text Int+ -- ^ What DataSources had requests that were being waited for+ , fetchWaitStart :: {-# UNPACK #-} !Timestamp+ , fetchWaitDuration :: {-# UNPACK #-} !Microseconds+ }+ | FetchDataSourceStats+ { fetchDsStatsCallId :: CallId+ , fetchDsStatsDataSource :: Text+ , fetchDsStatsStats :: DataSourceStats+ , fetchBatchId :: {-# UNPACK #-} !Int+ }+ deriving (Eq, 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+ppFetchStats MemoCall{} = ""+ppFetchStats FetchWait{..}+ | HashMap.size fetchWaitReqs == 0 = msg "unexpected: Blocked on nothing"+ | HashMap.size fetchWaitReqs <= 2 =+ msg $ printf "Blocked on %s"+ (intercalate "," [printf "%s (%d reqs)" ds c+ | (ds,c) <- HashMap.toList fetchWaitReqs])+ | otherwise = msg $ printf "Blocked on %d sources (%d reqs)"+ (HashMap.size fetchWaitReqs)+ (sum $ HashMap.elems fetchWaitReqs)+ where+ msg :: String -> String+ msg x = printf "%s (%.2fms)"+ x+ (fromIntegral fetchWaitDuration / 1000 :: Double)+ppFetchStats FetchDataSourceStats{..} =+ printf "%s (stats): %s" (Text.unpack fetchDsStatsDataSource)+ (show fetchDsStatsStats)++-- | Aggregate stats merging FetchStats from the same dispatched batch into one.+aggregateFetchBatches :: ([FetchStats] -> a) -> Stats -> [a]+aggregateFetchBatches agg (Stats fetches) =+ map agg $+ groupBy ((==) `on` fetchBatchId) $+ sortOn (Down . fetchBatchId)+ [f | f@FetchStats{} <- fetches]++instance ToJSON FetchStats where+ toJSON FetchStats{..} = object+ [ "type" .= ("FetchStats" :: Text)+ , "datasource" .= fetchDataSource+ , "fetches" .= fetchBatchSize+ , "start" .= fetchStart+ , "duration" .= fetchDuration+ , "allocation" .= fetchSpace+ , "failures" .= fetchFailures+ , "ignoredFailures" .= fetchIgnoredFailures+ , "batchid" .= fetchBatchId+ , "fetchids" .= fetchIds+ ]+ toJSON (FetchCall req strs fid) = object+ [ "type" .= ("FetchCall" :: Text)+ , "request" .= req+ , "stack" .= strs+ , "fetchid" .= fid+ ]+ toJSON (MemoCall cid allocs) = object+ [ "type" .= ("MemoCall" :: Text)+ , "callid" .= cid+ , "allocation" .= allocs+ ]+ toJSON FetchWait{..} = object+ [ "type" .= ("FetchWait" :: Text)+ , "duration" .= fetchWaitDuration+ ]+ toJSON FetchDataSourceStats{..} = object+ [ "type" .= ("FetchDataSourceStats" :: Text)+ , "datasource" .= fetchDsStatsDataSource+ , "stats" .= sjson fetchDsStatsStats+ , "batchid" .= fetchBatchId+ ]+ where+ sjson (DataSourceStats s) = toJSON s++emptyStats :: Stats+emptyStats = Stats []++numFetches :: Stats -> Int+numFetches (Stats rs) = sum [ fetchBatchSize | FetchStats{..} <- rs ]+++-- ---------------------------------------------------------------------------+-- Profiling++type ProfileLabel = Text+type AllocCount = Int64+type LabelHitCount = Int64+type ProfileKey = Int64++data ProfileFetch = ProfileFetch+ { profileFetchFetchId :: {-# UNPACK #-} !CallId+ , profileFetchMemoId :: {-# UNPACK #-} !CallId+ , profileFetchWasCached :: !Bool+ }+ deriving (Show, Eq)++data ProfileMemo = ProfileMemo+ { profileMemoId :: {-# UNPACK #-} !CallId+ , profileMemoWasCached :: !Bool+ }+ deriving (Show, Eq)++data Profile = Profile+ { profile :: HashMap ProfileKey ProfileData+ -- ^ Data per key (essentially per call stack)+ , profileTree :: HashMap (ProfileLabel, ProfileKey) ProfileKey+ -- ^ (label, parent) -> current. The exception is the root which will have+ -- ("MAIN", 0) -> 0+ , profileNextKey :: ProfileKey+ -- ^ Provides a unique key per callstack+ }++emptyProfile :: Profile+emptyProfile = Profile HashMap.empty (HashMap.singleton ("MAIN", 0) 0) 1++data ProfileData = ProfileData+ { profileAllocs :: {-# UNPACK #-} !AllocCount+ -- ^ allocations made by this label+ , profileFetches :: [ProfileFetch]+ -- ^ fetches made in this label+ , profileLabelHits :: {-# UNPACK #-} !LabelHitCount+ -- ^ number of hits at this label+ , profileMemos :: [ProfileMemo]+ -- ^ memo and a boolean representing if it was cached at the time+ , profileTime :: {-# UNPACK #-} !Microseconds+ -- ^ amount of time spent in computation at this label+ }+ deriving Show++emptyProfileData :: ProfileData+emptyProfileData = ProfileData 0 [] 0 [] 0
− Haxl/Core/Types.hs
@@ -1,363 +0,0 @@--- Copyright (c) 2014, 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 ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}---- | Base types used by all of Haxl.-module Haxl.Core.Types (-- -- * Initialization strategies- InitStrategy(..),-- -- * Tracing flags- Flags(..),- defaultFlags,- ifTrace,-- -- * Statistics- Stats(..),- RoundStats(..),- emptyStats,- numRounds,- numFetches,-- -- * Data fetching- DataSource(..),- DataSourceName(..),- Request,- BlockedFetch(..),- PerformFetch(..),-- -- * Result variables- ResultVar(..),- newEmptyResult,- newResult,- putFailure,- putResult,- putSuccess,- takeResult,- tryReadResult,- tryTakeResult,-- -- * Default fetch implementations- asyncFetch, asyncFetchWithDispatch,- stubFetch,- syncFetch,-- -- * Utilities- except,- setError,-- ) where--import Control.Applicative-import Control.Exception-import Data.Typeable-import Data.Text (Text)-import Data.Aeson-import Data.Hashable-import Control.Concurrent.MVar-import Control.Monad-import qualified Data.HashMap.Strict as HashMap-import Data.HashMap.Strict (HashMap)--#if __GLASGOW_HASKELL__ < 708-import Haxl.Core.Util (tryReadMVar)-#endif-import Haxl.Core.Show1-import Haxl.Core.StateStore---- | Initialization strategy. 'FullInit' will do as much initialization as--- possible. 'FastInit' will postpone part of initialization or omit part of--- initialization by sharing more resources. Use 'FastInit' if you want--- fast initialization but don't care much about performance, for example, in--- interactive environment.-data InitStrategy- = FullInit- | FastInit- deriving (Enum, Eq, Show)---- | Flags that control the operation of the engine.-data Flags = Flags- { trace :: Int- -- ^ Tracing level (0 = quiet, 3 = very verbose).- }--defaultFlags :: Flags-defaultFlags = Flags { trace = 0 }---- | Runs an action if the tracing level is above the given threshold.-ifTrace :: (Functor m, Monad m) => Flags -> Int -> m a -> m ()-ifTrace flags i m- | trace flags >= i = void m- | otherwise = return ()---- | Stats that we collect along the way.-newtype Stats = Stats [RoundStats]- deriving ToJSON---- | 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.-newtype RoundStats = RoundStats (HashMap Text Int)- deriving ToJSON--fetchesInRound :: RoundStats -> Int-fetchesInRound (RoundStats hm) = sum $ HashMap.elems hm--emptyStats :: Stats-emptyStats = Stats []--numRounds :: Stats -> Int-numRounds (Stats rs) = length rs--numFetches :: Stats -> Int-numFetches (Stats rs) = sum (map fetchesInRound rs)---- | 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://phabricator.fb.com/diffusion/FBCODE/browse/master/sigma/haxl/core/tests/ExampleDataSource.hs ExampleDataSource>.----class (DataSourceName req, StateKey req, Show1 req) => DataSource u req where-- -- | Issues a list of fetches to this 'DataSource'. The 'BlockedFetch'- -- objects contain both the request and the 'MVar'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 'Show1' 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). 'Show1' 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 ())---- | A 'BlockedFetch' is a pair of------ * The request to fetch (with result type @a@)------ * An 'MVar' 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 'MVar'--- out, the type system knows that the result type of the request--- matches the type parameter of the 'MVar', so it will let us take the--- result of the request and store it in the 'MVar'.----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, used by 'BlockedFetch' and the--- 'DataCache'. 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.----newtype ResultVar a = ResultVar (MVar (Either SomeException a))--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---- | 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
@@ -1,40 +1,42 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. -{-# LANGUAGE CPP #-}+ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-} +-- | Internal utilities only.+--+{-# OPTIONS_GHC -fno-warn-unused-imports #-} module Haxl.Core.Util- ( compose+ ( atomicallyOnBlocking+ , compose , textShow- , tryReadMVar+ , trace_ ) where -#if __GLASGOW_HASKELL__ >= 708-import Control.Concurrent (tryReadMVar)-#else-import Control.Concurrent-#endif- import Data.Text (Text)-+import Debug.Trace (trace) import qualified Data.Text as Text +import Control.Concurrent.STM+import Control.Exception++atomicallyOnBlocking :: Exception e => e -> STM a -> IO a+atomicallyOnBlocking e stm =+ catch (atomically stm)+ (\BlockedIndefinitelyOnSTM -> throw e)+ -- | 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++-- | This function can be used to trace a bunch of lines to stdout when+-- debugging haxl core.+trace_ :: String -> a -> a+trace_ _ = id+--trace_ = Debug.Trace.trace
+ Haxl/DataSource/ConcurrentIO.hs view
@@ -0,0 +1,83 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# 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 w 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.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) ->+ forkFinally (performIO req) (putResultFromChildThread rv)
Haxl/Prelude.hs view
@@ -1,10 +1,11 @@--- Copyright (c) 2014, 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.+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved. + This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -12,7 +13,7 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Support for using Haxl as a DSL. This module provides most of--- the standard Prelude, plus a selection of stuff that makes it+-- the standard Prelude, plus a selection of stuff that makes -- Haxl client code cleaner and more concise. -- -- We intend Haxl client code to:@@ -31,15 +32,17 @@ module Prelude, -- * Haxl and Fetching data- GenHaxl, dataFetch, DataSource,+ GenHaxl, dataFetch, DataSource, memo,+ memoize, memoize1, memoize2, -- * Extra Monad and Applicative things Applicative(..),- mapM, mapM_, sequence, sequence_, (<$>), filterM, foldM,+ mapM, mapM_, sequence, sequence_, filterM, foldM, forM, forM_, foldl', sort, Monoid(..), join,+ andThen, -- * Lifted operations IfThenElse(..),@@ -47,6 +50,7 @@ (.==), (./=), (.&&), (.||), (.++), pair,+ pAnd, pOr, -- * Text things Text,@@ -60,18 +64,20 @@ ) 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)+import Control.Monad (foldM, join, void) import Data.List (foldl', sort) import Data.Text (Text) import Data.Traversable hiding (forM, mapM, sequence) import GHC.Exts (IsString(..)) import Prelude hiding (mapM, mapM_, sequence, sequence_)-import Data.Monoid import Data.Maybe import Control.Exception (fromException) @@ -89,19 +95,19 @@ ifThenElse :: a -> b -> b -> b instance IfThenElse Bool a where- ifThenElse b t e = case b of True -> t; False -> e+ ifThenElse b t e = if b then t else e -- The equality constraint is necessary to convince the typechecker that -- this is valid: ----- > if getCountry ip .== "us" then ... else ...+-- > if ipGetCountry ip .== "us" then ... else ... ---instance (u1 ~ u2) => IfThenElse (GenHaxl u1 Bool) (GenHaxl u2 a) where+instance (u1 ~ u2) => IfThenElse (GenHaxl u1 w Bool) (GenHaxl u2 w a) where ifThenElse fb t e = do b <- fb if b then t else e -instance Num a => Num (GenHaxl u a) where+instance Num a => Num (GenHaxl u w a) where (+) = liftA2 (+) (-) = liftA2 (-) (*) = liftA2 (*)@@ -110,7 +116,7 @@ signum = liftA signum negate = liftA negate -instance Fractional a => Fractional (GenHaxl u a) where+instance Fractional a => Fractional (GenHaxl u w a) where (/) = liftA2 (/) recip = liftA recip fromRational = return . fromRational@@ -121,35 +127,35 @@ -- convention is to prefix the name with a '.'. We could change this, -- or even just not provide these at all. -(.>) :: Ord a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(.>) :: Ord a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (.>) = liftA2 (Prelude.>) -(.<) :: Ord a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(.<) :: Ord a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (.<) = liftA2 (Prelude.<) -(.>=) :: Ord a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(.>=) :: Ord a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (.>=) = liftA2 (Prelude.>=) -(.<=) :: Ord a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(.<=) :: Ord a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (.<=) = liftA2 (Prelude.<=) -(.==) :: Eq a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(.==) :: Eq a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (.==) = liftA2 (Prelude.==) -(./=) :: Eq a => GenHaxl u a -> GenHaxl u a -> GenHaxl u Bool+(./=) :: Eq a => GenHaxl u w a -> GenHaxl u w a -> GenHaxl u w Bool (./=) = liftA2 (Prelude./=) -(.++) :: GenHaxl u [a] -> GenHaxl u [a] -> GenHaxl u [a]+(.++) :: GenHaxl u w [a] -> GenHaxl u w [a] -> GenHaxl u w [a] (.++) = liftA2 (Prelude.++) -- short-circuiting Bool operations-(.&&):: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool+(.&&):: GenHaxl u w Bool -> GenHaxl u w Bool -> GenHaxl u w Bool fa .&& fb = do a <- fa; if a then fb else return False -(.||):: GenHaxl u Bool -> GenHaxl u Bool -> GenHaxl u Bool+(.||):: GenHaxl u w Bool -> GenHaxl u w Bool -> GenHaxl u w Bool fa .|| fb = do a <- fa; if a then return True else fb -pair :: GenHaxl u a -> GenHaxl u b -> GenHaxl u (a, b)+pair :: GenHaxl u w a -> GenHaxl u w b -> GenHaxl u w (a, b) pair = liftA2 (,) -- -----------------------------------------------------------------------------@@ -167,7 +173,7 @@ -- | See 'mapM'. mapM_ :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f ()-mapM_ f t = const () <$> traverse f t+mapM_ f t = void $ traverse f t forM_ :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f () forM_ = flip mapM_@@ -178,28 +184,48 @@ -- | See 'mapM'. sequence_ :: (Traversable t, Applicative f) => t (f a) -> f ()-sequence_ t = const () <$> sequenceA t+sequence_ t = void $ sequenceA t -- | See 'mapM'.-filterM :: (Applicative f, Monad f) => (a -> f Bool) -> [a] -> f [a]-filterM pred xs = do- bools <- mapM pred xs- return [ x | (x,True) <- zip xs bools ]+filterM :: (Applicative f) => (a -> f Bool) -> [a] -> f [a]+filterM predicate xs =+ filt <$> mapM predicate xs+ where+ filt bools = [ x | (x,True) <- zip xs bools ] +-- | In somes cases, we do want the monadic version of @('>>')@ to disable+-- concurrency and start one computation only after the other finishes, e.g.:+--+-- @+-- deferedFetch x = do+-- sleep 5+-- fetch x -- fetch will actually run concurrently with sleep+-- @+--+-- But we have defined @('>>') = ('*>')@ with the applicative behavior as this+-- is desired in most cases, so instead we define 'andThen' as the monadic+-- version of @('>>')@:+--+-- @+-- deferedFetch x = sleep 5 `andThen` fetch x+-- @+andThen :: Monad m => m a -> m b -> m b+andThen a b = a >>= \_ -> b+ -------------------------------------------------------------------------------- -- | Runs the given 'GenHaxl' computation, and if it throws a -- 'TransientError' or 'LogicError' exception (see -- "Haxl.Core.Exception"), the exception is ignored and the supplied -- default value is returned instead.-withDefault :: a -> GenHaxl u a -> GenHaxl u a+withDefault :: a -> GenHaxl u w a -> GenHaxl u w a withDefault d a = catchAny a (return d) -- | Catch 'LogicError's and 'TransientError's and perform an alternative action catchAny- :: GenHaxl u a -- ^ run this first- -> GenHaxl u a -- ^ if it throws 'LogicError' or 'TransientError', run this- -> GenHaxl u a+ :: GenHaxl u w a -- ^ run this first+ -> GenHaxl u w a -- ^ if it throws 'LogicError' or 'TransientError', run this+ -> GenHaxl u w a catchAny haxl handler = haxl `catch` \e -> if isJust (fromException e :: Maybe LogicError) ||
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2014, Facebook, Inc.+Copyright (c) 2014-present, Facebook, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification,
− PATENTS
@@ -1,23 +0,0 @@-Additional Grant of Patent Rights--"Software" means the Haxl software distributed by Facebook, Inc.--Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive,-irrevocable (subject to the termination provision below) license under any-rights in any patent claims owned by Facebook, 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 a third party,-or (ii) the Software in combination with any software or other technology-provided by you or a third party.--The license granted hereunder will terminate, automatically and without notice,-for anyone that makes any claim (including by filing any lawsuit, assertion or-other action) alleging (a) direct, indirect, or contributory infringement or-inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or-affiliates, whether or not such claim is related to the Software, (ii) by any-party if such claim arises in whole or in part from any software, product or-service of Facebook or any of its subsidiaries or affiliates, whether or not-such claim is related to the Software, or (iii) by any party relating to the-Software; or (b) that any right in any patent claim of Facebook is invalid or-unenforceable.
Setup.hs view
@@ -1,2 +1,11 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++module Setup where import Distribution.Simple main = defaultMain
+ changelog.md view
@@ -0,0 +1,109 @@+# Changes in version 2.5.1.1+ * Bump dependencies; builds up to GHC 9.10++# Changes in version 2.5.1.0+ * Add schedulerHintState method to DataSource++# Changes in version 2.4.0.0+ * Added fetchBatchId to FetchStats+ * Profiling now tracks full stacks and links each label to memos/fetches+ * Adds FetchDataSourceStats used to log stats/profiling data returned+ from datasources. This is stored in statsRef like any other Stats.+ * Report flag was changed from sequential numbers to bitmask.+ * Add ReportExceptionLabelStack flag to include label stack in HaxlException.++# Changes in version 2.3.0.0+ * Removed `FutureFetch`++# Changes in version 2.2.0.0++ * Use BasicHashTable for the Haxl DataCache instead of HashMap+ * API Changes in: Haxl.Core.DataCache, Haxl.Core.Fetch+ * Removed support for GHC < 8.2++# Changes in version 2.1.2.0++ * Add a callgraph reference to 'Env' to record the function callgraph during a+ computation. The callgraph is stored as an edge list in the Env through the+ use of `withCallGraph` and enables users to debug a Haxl computation.++# Changes in version 2.1.1.0+ * Adds feature to track outgone datasource fetches. This is only turned on+ for report level greater than 1. The fetches are stored as a running Map+ in the env ('submittedReqsRef').++# Changes in version 2.1.0.0++ * Add a new 'w' parameter to 'GenHaxl' to allow arbitrary writes during+ a computation. These writes are stored as a running log in the Env,+ and are memoized. This allows users to extract information from+ a Haxl computation which throws. Our advise is to limit these writes to+ monitoring and debugging logs.++ * A 'WriteTree' constructor to maintain log of writes inside the Environment.+ This is defined to allow O(1) mappend.++# Changes in version 2.0.1.1++ * Support for GHC 8.6.1+ * Bugfixes++# Changes in version 2.0.1.0++ * Exported MemoVar from Haxl.Core.Memo+ * Updated the facebook example+ * Fixed some links in the documentation+ * Bump some version bounds++# 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+ * 'asyncFetchAcquireRelease' was added+ * 'cacheResultWithShow' was exposed+ * GHC 8.2.1 compatibility++# Changes in version 0.5.0.0+ * Rename 'Show1' to 'ShowP' ([#62](https://github.com/facebook/Haxl/issues/62))++# Changes in version 0.3.0.0++ * Some performance improvements, including avoiding quadratic+ slowdown with left-associated binds.++ * Documentation cleanup; Haxl.Core is the single entry point for the+ core and engine docs.++ * (>>) is now defined to be (*>), and therefore no longer forces+ sequencing. This can have surprising consequences if you are+ using Haxl with side-effecting data sources, so watch out!++ * New function withEnv, for running a sub-computation in a local Env++ * Add a higher-level memoization API, see 'memo'++ * Show is no longer required for keys in cachedComputation++ * Exceptions now have `Eq` instances
haxl.cabal view
@@ -1,79 +1,123 @@ name: haxl-version: 0.1.0.0+version: 2.5.1.1 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: BSD3-license-file: LICENSE+license-files: LICENSE author: Facebook, Inc. maintainer: The Haxl Team <haxl-team@fb.com>-copyright: 2014 (C) 2014 Facebook, Inc.+copyright: Copyright (c) 2014-present, Facebook, Inc. category: Concurrency build-type: Simple stability: alpha cabal-version: >= 1.10+tested-with:+ GHC==8.4.4+ GHC==8.6.5+ GHC==8.8.4+ GHC==8.10.7+ GHC==9.0.2+ GHC==9.2.8+ GHC==9.4.8+ GHC==9.6.6+ GHC==9.8.2+ GHC==9.10.1 description: Haxl is a library and EDSL for efficient scheduling of concurrent data accesses with a concise applicative API.+ .+ To use Haxl, you need to implement one or more /data sources/, which+ provide the means for accessing remote data or other I/O that you+ want to perform using Haxl.+ .+ Haxl provides two top-level modules:+ .+ * /Data-source implementations/ import "Haxl.Core",+ .+ * /Client code/ import your data sources and "Haxl.Prelude", or some+ other client-level API that you provide. extra-source-files: readme.md- PATENTS tests/LoadCache.txt+ changelog.md library build-depends:- HUnit == 1.2.*,- aeson >= 0.6 && <0.8,- base == 4.*,- bytestring >= 0.9 && <0.11,- containers == 0.5.*,- directory >= 1.1 && <1.3,- filepath == 1.3.*,- hashable == 1.2.*,+ aeson >= 0.6 && < 2.3,+ base >= 4.10 && < 5,+ binary >= 0.7 && < 0.10,+ bytestring >= 0.9 && < 0.13,+ containers >= 0.5 && < 0.8,+ deepseq,+ exceptions >=0.8 && <0.11,+ filepath >= 1.3 && < 1.6,+ ghc-prim,+ hashable >= 1.2 && < 1.6,+ hashtables >= 1.2.3.1, pretty == 1.1.*,- text == 1.1.*,- time == 1.4.*,+ -- text 1.2.1.0 required for instance Binary Text+ text >= 1.2.1.0 && < 1.3 || >= 2 && < 2.2,+ time >= 1.4 && < 1.13,+ stm >= 2.4 && < 2.6,+ transformers, unordered-containers == 0.2.*,- vector == 0.10.*+ vector >= 0.10 && <0.14 exposed-modules: Haxl.Core,+ Haxl.Core.CallGraph, Haxl.Core.DataCache,+ Haxl.Core.DataSource, Haxl.Core.Exception,- Haxl.Core.Env,- Haxl.Core.Fetch,+ 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.ShowP, Haxl.Core.StateStore,- Haxl.Core.Show1,- Haxl.Core.Types,+ Haxl.Core.Stats, Haxl.Prelude+ Haxl.DataSource.ConcurrentIO other-modules: Haxl.Core.Util default-language: Haskell2010+ default-extensions:+ TypeOperators ghc-options:+ -O2 -fprof-auto -Wall- -fno-warn-name-shadowing-+ -Wno-name-shadowing test-suite test build-depends:- HUnit, aeson,- base == 4.*,+ HUnit >= 1.2 && < 1.7,+ base >= 4.7 && < 5,+ binary, bytestring, containers,+ deepseq,+ filepath, hashable,+ hashtables, haxl,+ test-framework,+ test-framework-hunit, text,+ time, unordered-containers ghc-options:@@ -85,17 +129,83 @@ tests main-is:- Main.hs+ TestMain.hs other-modules:+ AdoTests+ AllTests+ BadDataSource BatchTests CoreTests DataCacheTest ExampleDataSource+ ExceptionStackTests+ FullyAsyncTest LoadCache+ MemoizationTests MockTAO+ MonadAsyncTest+ OutgoneFetchesTests+ ParallelTests+ ProfileTests+ SleepDataSource+ StatsTests+ TestBadDataSource TestExampleDataSource TestTypes+ TestUtils+ WorkDataSource+ WriteTests+ DataSourceDispatchTests type: exitcode-stdio-1.0++ default-language: Haskell2010+ default-extensions:+ TypeOperators++flag bench+ default: False++executable monadbench+ if !flag(bench)+ buildable: False+ default-language:+ Haskell2010+ default-extensions:+ TypeOperators+ hs-source-dirs:+ tests+ build-depends:+ base,+ haxl,+ hashable,+ time,+ optparse-applicative+ main-is:+ MonadBench.hs+ other-modules:+ ExampleDataSource+ ghc-options:+ -O2 -main-is MonadBench -rtsopts++executable cachebench+ if !flag(bench)+ buildable: False+ default-language:+ Haskell2010+ default-extensions:+ TypeOperators+ hs-source-dirs:+ tests+ build-depends:+ base,+ haxl,+ hashable,+ hashtables,+ time+ main-is:+ Bench.hs+ ghc-options:+ -O2 -main-is Bench -rtsopts
readme.md view
@@ -1,13 +1,16 @@-+ # Haxl +[](https://opensource.fb.com/support-ukraine) [](https://travis-ci.org/facebook/Haxl)+ Haxl is a Haskell library that simplifies access to remote data, such as databases or web-based services. Haxl can automatically * batch multiple requests to the same data source, * request data from multiple data sources concurrently,- * cache previous requests.+ * cache previous requests,+ * memoize computations. Having all this handled for you behind the scenes means that your data-fetching code can be much cleaner and clearer than it would@@ -17,33 +20,48 @@ There are two Haskell packages here: * `haxl`: The core Haxl framework- * `haxl-facebook` (in [example/facebook](example/facebook)): An (incomplete) example data source for accessing the Facebook Graph API+ * `haxl-facebook` (in [https://github.com/facebook/Haxl/tree/master/example/facebook](example/facebook)): An (incomplete) example data source for accessing the Facebook Graph API 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. ## Where to go next? - * *The Story of Haxl* (a blog post to be published very soon!)+ * [The Story of Haxl](https://code.facebook.com/posts/302060973291128/open-sourcing-haxl-a-library-for-haskell/) explains how Haxl came about at Facebook, and discusses our particular use case. - * [An example Facebook data source](example/facebook/readme.md) walks+ * [An example Facebook data source](https://github.com/facebook/Haxl/blob/master/example/facebook/readme.md) walks through building an example data source that queries the Facebook Graph API concurrently. - * [The N+1 Selects Problem](example/sql/readme.md) explains how Haxl+ * [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](https://github.com/facebook/Haxl/blob/master/example/sql/readme.md) explains how Haxl can address a common performance problem with SQL queries by automatically batching multiple queries into a single query,- completely invisibly to the programmer.+ without the programmer having to specify this behavior. * [Haxl Documentation](http://hackage.haskell.org/package/haxl) on Hackage. - * *There is no Fork: An Abstraction for Efficient, Concurrent, and- Concise Data Access*, our paper on Haxl, accepted for publication- at ICFP'14. Online version coming soon (June 12).+ * [There is no Fork: An Abstraction for Efficient, Concurrent, and Concise Data Access](http://simonmar.github.io/bib/papers/haxl-icfp14.pdf), our paper on Haxl, accepted for publication at ICFP'14.++## Contributing++We welcome contributions! See [CONTRIBUTING](https://github.com/facebook/Haxl/blob/master/CONTRIBUTING.md) for details on how to get started, and our [Code of Conduct](https://github.com/facebook/Haxl/blob/master/CODE_OF_CONDUCT.md).++## License++Haxl uses the BSD 3-clause License, as found in the [LICENSE](https://github.com/facebook/Haxl/blob/master/LICENSE) file.
+ tests/AdoTests.hs view
@@ -0,0 +1,57 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE RebindableSyntax, OverloadedStrings, ApplicativeDo #-}+module AdoTests (tests) where++import TestUtils+import MockTAO++import Control.Applicative+import Test.HUnit++import Prelude()+import Haxl.Prelude++-- -----------------------------------------------------------------------------++--+-- Test ApplicativeDo batching+--+ado1 = expectResult 12 ado1_++ado1_ = do+ a <- friendsOf =<< id1+ b <- friendsOf =<< id2+ return (length (a ++ b))++ado2 = expectResult 12 ado2_++ado2_ = do+ x <- id1+ a <- friendsOf x+ y <- id2+ b <- friendsOf y+ return (length (a ++ b))++ado3 = expectResult 11 ado3_++ado3_ = do+ x <- id1+ a <- friendsOf x+ a' <- friendsOf =<< if null a then id3 else id4+ y <- id2+ b <- friendsOf y+ b' <- friendsOf =<< if null b then id4 else id3+ return (length (a' ++ b'))++tests future = TestList+ [ TestLabel "ado1" $ TestCase (ado1 future)+ , TestLabel "ado2" $ TestCase (ado2 future)+ , TestLabel "ado3" $ TestCase (ado3 future)+ ]
+ tests/AllTests.hs view
@@ -0,0 +1,50 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+module AllTests (allTests) where++import TestExampleDataSource+import BatchTests+import CoreTests+import DataCacheTest+import ExceptionStackTests+import AdoTests+import OutgoneFetchesTests+import ProfileTests+import MemoizationTests+import MonadAsyncTest+import TestBadDataSource+import FullyAsyncTest+import WriteTests+import ParallelTests+import StatsTests+import DataSourceDispatchTests++import Test.HUnit++allTests :: Test+allTests = TestList+ [ TestLabel "ExampleDataSource" TestExampleDataSource.tests+ , TestLabel "BatchTests-future" $ BatchTests.tests True+ , TestLabel "BatchTests-sync" $ BatchTests.tests False+ , TestLabel "CoreTests" CoreTests.tests+ , TestLabel "DataCacheTests" DataCacheTest.tests+ , TestLabel "ExceptionStackTests" ExceptionStackTests.tests+ , TestLabel "AdoTests" $ AdoTests.tests False+ , TestLabel "OutgoneFetchesTest" OutgoneFetchesTests.tests+ , TestLabel "ProfileTests" ProfileTests.tests+ , TestLabel "MemoizationTests" MemoizationTests.tests+ , TestLabel "MonadAsyncTests" MonadAsyncTest.tests+ , TestLabel "BadDataSourceTests" TestBadDataSource.tests+ , TestLabel "FullyAsyncTest" FullyAsyncTest.tests+ , TestLabel "WriteTest" WriteTests.tests+ , TestLabel "ParallelTest" ParallelTests.tests+ , TestLabel "StatsTests" StatsTests.tests+ , TestLabel "DataSourceDispatchTests" DataSourceDispatchTests.tests+ ]
+ tests/BadDataSource.hs view
@@ -0,0 +1,132 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-}++-- | A data source that can be made to fail in various ways, for testing++module BadDataSource (+ -- * initialise the state+ State(..), initGlobalState, FetchImpl(..),++ -- * requests for this data source+ FailAfter(..)+ ) where++import Haxl.Prelude+import Prelude ()++import Haxl.Core++import Control.Exception+import Data.Typeable+import Data.Hashable+import Control.Concurrent+import Control.Monad (void)++import GHC.Conc ( PrimMVar )+import Foreign.StablePtr+import Foreign.C.Types ( CInt(..) )++foreign import ccall safe+ hs_try_putmvar :: CInt -> StablePtr PrimMVar -> IO ()++data FetchImpl =+ Async+ | Background+ | BackgroundMVar+ | BackgroundSeq+ | BackgroundPar++data FailAfter a where+ FailAfter :: Int -> FailAfter Int+ deriving Typeable++deriving instance Eq (FailAfter a)+deriving instance Show (FailAfter a)+instance ShowP FailAfter where showp = show++instance Hashable (FailAfter a) where+ hashWithSalt s (FailAfter a) = hashWithSalt s (0::Int,a)++instance StateKey FailAfter where+ data State FailAfter = FailAfterState+ { failAcquireDelay :: Int+ , failAcquire :: IO ()+ , failReleaseDelay :: Int+ , failRelease :: IO ()+ , failDispatchDelay :: Int+ , failDispatch :: IO ()+ , failWaitDelay :: Int+ , failWait :: IO ()+ , failImpl :: FetchImpl+ }++instance DataSourceName FailAfter where+ dataSourceName _ = "BadDataSource"+++instance DataSource u FailAfter where+ fetch state@FailAfterState{..}+ | BackgroundSeq <- failImpl = backgroundFetchSeq runOne state+ | BackgroundPar <- failImpl = backgroundFetchPar runOne state+ | Background <- failImpl = backgroundFetchAcquireRelease+ acquire release dispatchbg wait+ submit state+ | BackgroundMVar <- failImpl = backgroundFetchAcquireReleaseMVar+ acquire release dispatchbgMVar wait+ submit state+ | Async <- failImpl = asyncFetchAcquireRelease+ acquire release dispatch wait+ submit state+ where+ acquire = do threadDelay failAcquireDelay; failAcquire+ release _ = do threadDelay failReleaseDelay; failRelease+ dispatch _ = do threadDelay failDispatchDelay; failDispatch+ dispatchBase put = (do+ failDispatch+ _ <- mask_ $ forkIO $ finally+ (threadDelay failDispatchDelay)+ put+ return ()) `onException` put+ dispatchbg _ c m = dispatchBase (hs_try_putmvar (fromIntegral c) m)+ dispatchbgMVar _ _ m = dispatchBase (void $ tryPutMVar m ())+ wait _ = do threadDelay failWaitDelay; failWait+ submit :: () -> FailAfter a -> IO (IO (Either SomeException a))+ submit _ (FailAfter t) = do+ threadDelay t+ return (return (Left (toException (FetchError "failed request"))))+ runOne :: FailAfter a -> IO (Either SomeException a)+ runOne r = do+ bracket acquire release $ \s -> do+ dispatch s+ getRes <- submit s r+ wait s+ getRes++initGlobalState :: FetchImpl -> IO (State FailAfter)+initGlobalState impl = do+ return FailAfterState+ { failAcquireDelay = 0+ , failAcquire = return ()+ , failReleaseDelay = 0+ , failRelease = return ()+ , failDispatchDelay = 0+ , failDispatch = return ()+ , failWaitDelay = 0+ , failWait = return ()+ , failImpl = impl+ }
tests/BatchTests.hs view
@@ -1,71 +1,33 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE RebindableSyntax, OverloadedStrings #-} module BatchTests (tests) where import TestTypes+import TestUtils import MockTAO import Control.Applicative-import Data.IORef-import Data.Aeson import Test.HUnit-import qualified Data.HashMap.Strict as HashMap import Haxl.Core import Prelude() import Haxl.Prelude+import Data.IORef -- ----------------------------------------------------------------------------- -testinput :: Object-testinput = HashMap.fromList [- "A" .= (1 :: Int),- "B" .= (2 :: Int),- "C" .= (3 :: Int),- "D" .= (4 :: Int) ]--makeTestEnv :: IO (Env UserEnv)-makeTestEnv = do- tao <- MockTAO.initGlobalState- let st = stateSet tao stateEmpty- initEnv st testinput--expectRoundsWithEnv- :: (Eq a, Show a) => Int -> a -> Haxl a -> Env UserEnv -> Assertion-expectRoundsWithEnv n result haxl env = do- a <- runHaxl env haxl- assertEqual "result" result a- 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- expectRoundsWithEnv n result haxl env--expectFetches :: (Eq a, Show a) => Int -> Haxl a -> Assertion-expectFetches n haxl = do- env <- makeTestEnv- _ <- runHaxl env haxl- stats <- readIORef (statsRef env)- assertEqual "fetches" n (numFetches stats)--friendsOf :: Id -> Haxl [Id]-friendsOf = assocRangeId2s friendsAssoc--id1 :: Haxl Id-id1 = lookupInput "A"--id2 :: Haxl Id-id2 = lookupInput "B"--id3 :: Haxl Id-id3 = lookupInput "C"- -- -- Test batching over multiple arguments in liftA2 ---batching1 = expectRounds 1 12 batching1_+batching1 = expectResult 12 batching1_ batching1_ = do a <- id1@@ -75,7 +37,7 @@ -- -- Test batching in mapM (which is really traverse) ---batching2 = expectRounds 1 12 batching2_+batching2 = expectResult 12 batching2_ batching2_ = do a <- id1@@ -86,7 +48,7 @@ -- -- Test batching when we have a monadic bind in each branch ---batching3 = expectRounds 1 12 batching3_+batching3 = expectResult 12 batching3_ batching3_ = do let a = id1 >>= friendsOf@@ -96,7 +58,7 @@ -- -- Test batching over both arguments of (+) ---batching4 = expectRounds 1 12 batching4_+batching4 = expectResult 12 batching4_ batching4_ = do let a = length <$> (id1 >>= friendsOf)@@ -106,7 +68,7 @@ -- -- Test batching over both arguments of (+) ---batching5 = expectRounds 1 2 batching5_+batching5 = expectResult 2 batching5_ batching5_ :: Haxl Int batching5_ = if a .> b then 1 else 2@@ -117,14 +79,14 @@ -- -- Test batching when we perform all batching tests together with sequence ---batching6 = expectRounds 1 [12,12,12,12,2] batching6_+batching6 = expectResult [12,12,12,12,2] batching6_ batching6_ = sequence [batching1_,batching2_,batching3_,batching4_,batching5_] -- -- Ensure if/then/else and bool operators break batching ---batching7 = expectRounds 2 12 batching7_+batching7 = expectResult 12 batching7_ batching7_ :: Haxl Int batching7_ = if a .> 0 then a+b else 0@@ -133,7 +95,7 @@ b = length <$> (id2 >>= friendsOf) -- We expect 3 rounds here due to boolean operators-batching8 = expectRounds 3 12 batching8_+batching8 = expectResult 12 batching8_ batching8_ :: Haxl Int batching8_ = if (c .== 0) .|| (a .> 0 .&& b .> 0) then a+b else 0@@ -142,6 +104,12 @@ b = length <$> (id2 >>= friendsOf) c = length <$> (id3 >>= friendsOf) +-- (>>) should batch, so we expect one round+batching9 = expectResult 6 batching9_++batching9_ :: Haxl Int+batching9_ = (id1 >>= friendsOf) >> (length <$> (id2 >>= friendsOf))+ -- -- Test data caching, numFetches --@@ -167,45 +135,37 @@ -- -- Basic sanity check on data-cache re-use ---cacheReuse = do- env <- makeTestEnv- expectRoundsWithEnv 2 12 batching7_ env+cacheReuse future = do+ env <- makeTestEnv future+ expectResultWithEnv 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)+ cid <- readIORef (callIdRef env2)+ assertBool "callId is unique" (cid > 0) -- ensure no more data fetching rounds needed- expectRoundsWithEnv 0 12 batching7_ env2+ expectResultWithEnv 12 batching7_ env2 -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 "caching1" $ TestCase caching1- , TestLabel "caching2" $ TestCase caching2- , TestLabel "caching3" $ TestCase caching3- , TestLabel "CacheReuse" $ TestCase cacheReuse- , TestLabel "exceptionTest1" $ TestCase exceptionTest1- , TestLabel "exceptionTest2" $ TestCase exceptionTest2- , TestLabel "deterministicExceptions" $ TestCase deterministicExceptions- ]+noCaching future = do+ env <- makeTestEnv future+ let env' = env{ flags = (flags env){caching = 0} }+ result <- runHaxl env' caching3_+ assertEqual "result" result 18+ stats <- readIORef (statsRef env)+ assertEqual "fetches" 4 (numFetches stats) -exceptionTest1 = expectRounds 1 []+exceptionTest1 = expectResult [] $ withDefault [] $ friendsOf 101 -exceptionTest2 = expectRounds 1 [7..12] $ liftA2 (++)+exceptionTest2 = expectResult [7..12] $ liftA2 (++) (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")@@ -224,3 +184,124 @@ case r of Left (NotFound "xxx") -> True _ -> False++pOrTests future = do+ env <- makeTestEnv future++ -- Test semantics+ r <- runHaxl env $ do+ a <- return False `pOr` return False+ b <- return False `pOr` return True+ c <- return True `pOr` return False+ d <- return True `pOr` return True+ return (not a && b && c && d)+ assertBool "pOr0" r++ -- 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+ Right True -> True+ _ -> False+ r <- runHaxl env $ try $ throw (NotFound "foo") `pOr` return True+ assertBool "pOr2" $+ case (r :: Either NotFound Bool) of+ Left (NotFound "foo") -> True+ _ -> False++ -- pOr is non-deterministic (see also Note [tricky pOr/pAnd])+ let nondet = (do _ <- friendsOf 1; throw (NotFound "foo")) `pOr` return True+ r <- runHaxl env $ try nondet+ assertBool "pOr3" $+ case (r :: Either NotFound Bool) of+ Right True -> True+ _ -> False+ -- next we populate the cache+ _ <- runHaxl env $ friendsOf 1+ -- and now exactly the same pOr again will throw this time:+ r <- runHaxl env $ try nondet+ assertBool "pOr4" $+ case (r :: Either NotFound Bool) of+ Left (NotFound "foo") -> True+ _ -> False++ -- One more test: Blocked/False => Blocked+ r <- runHaxl env $ try $+ (do _ <- friendsOf 2; throw (NotFound "foo")) `pOr` return False+ assertBool "pOr5" $+ case (r :: Either NotFound Bool) of+ Left (NotFound _) -> True+ _ -> False++pAndTests future = do+ env <- makeTestEnv future++ -- Test semantics+ r <- runHaxl env $ do+ a <- return False `pAnd` return False+ b <- return False `pAnd` return True+ c <- return True `pAnd` return False+ d <- return True `pAnd` return True+ return (not a && not b && not c && d)+ assertBool "pAnd0" r++ -- 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+ Right False -> True+ _ -> False+ r <- runHaxl env $ try $ throw (NotFound "foo") `pAnd` return False+ assertBool "pAnd2" $+ case (r :: Either NotFound Bool) of+ Left (NotFound "foo") -> True+ _ -> False++ -- pAnd is non-deterministic (see also Note [tricky pOr/pAnd])+ let nondet =+ (do _ <- friendsOf 1; throw (NotFound "foo")) `pAnd` return False+ r <- runHaxl env $ try nondet+ assertBool "pAnd3" $+ case (r :: Either NotFound Bool) of+ Right False -> True+ _ -> False+ -- next we populate the cache+ _ <- runHaxl env $ friendsOf 1+ -- and now exactly the same pAnd again will throw this time:+ r <- runHaxl env $ try nondet+ assertBool "pAnd4" $+ case (r :: Either NotFound Bool) of+ Left (NotFound "foo") -> True+ _ -> False++ -- One more test: Blocked/True => Blocked+ r <- runHaxl env $ try $+ (do _ <- friendsOf 2; throw (NotFound "foo")) `pAnd` return True+ assertBool "pAnd5" $+ case (r :: Either NotFound Bool) of+ Left (NotFound _) -> True+ _ -> False++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
@@ -0,0 +1,68 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE RankNTypes, GADTs, BangPatterns, DeriveDataTypeable,+ StandaloneDeriving #-}+{-# OPTIONS_GHC -fno-warn-unused-do-bind -fno-warn-type-defaults #-}++module Bench where++import Haxl.Core.DataCache as DataCache++import Prelude hiding (mapM)++import Data.Hashable+import Data.IORef+import Data.Time.Clock+import Data.Traversable+import Data.Typeable+import System.Environment+import Text.Printf++data TestReq a where+ ReqInt :: {-# UNPACK #-} !Int -> TestReq Int+ ReqDouble :: {-# UNPACK #-} !Int -> TestReq Double+ ReqBool :: {-# UNPACK #-} !Int -> TestReq Bool+ deriving Typeable++deriving instance Eq (TestReq a)+deriving instance Show (TestReq a)++instance Hashable (TestReq a) where+ hashWithSalt salt (ReqInt i) = hashWithSalt salt (0::Int, i)+ hashWithSalt salt (ReqDouble i) = hashWithSalt salt (1::Int, i)+ hashWithSalt salt (ReqBool i) = hashWithSalt salt (2::Int, i)++main = do+ [n] <- fmap (fmap read) getArgs+ t0 <- getCurrentTime+ cache <- emptyDataCache+ let+ f 0 = return ()+ f !n = do+ m <- newIORef 0+ DataCache.insert (ReqInt n) m cache+ f (n-1)+ --+ f n+ m <- DataCache.lookup (ReqInt (n `div` 2)) cache+ print =<< mapM readIORef m+ t1 <- getCurrentTime+ printf "insert: %.2fs\n" (realToFrac (t1 `diffUTCTime` t0) :: Double)++ t0 <- getCurrentTime+ let+ f 0 !m = return m+ f !n !m = do+ mbRes <- DataCache.lookup (ReqInt n) cache+ case mbRes of+ Nothing -> f (n-1) m+ Just _ -> f (n-1) (m+1)+ f n 0 >>= print+ t1 <- getCurrentTime+ printf "lookup: %.2fs\n" (realToFrac (t1 `diffUTCTime` t0) :: Double)
tests/CoreTests.hs view
@@ -1,4 +1,15 @@-{-# LANGUAGE RebindableSyntax, OverloadedStrings #-}+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE RebindableSyntax #-}+ module CoreTests where import Haxl.Prelude@@ -10,17 +21,29 @@ import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as BS+import Data.List import Control.Exception (Exception(..)) -useless :: String -> GenHaxl u Bool-useless _ = throw (NotFound "ha ha")+import ExampleDataSource -en = error "no env"+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 () :: IO (Env () ())++useless :: String -> GenHaxl u w Bool+useless _ = throw (NotFound "ha ha")+ exceptions :: Assertion exceptions = do+ en <- emptyEnv () :: IO (Env () ()) a <- runHaxl en $ try (useless "input") assertBool "NotFound -> HaxlException" $ isLeft (a :: Either HaxlException Bool)@@ -48,14 +71,71 @@ f <- runHaxl en $ throw (NotFound "haha") `catch` \LogicError{} -> return True assertBool "catch2" f++ -- test catchIf+ let transientOrNotFound e+ | Just TransientError{} <- fromException e = True+ | Just NotFound{} <- fromException e = True+ | otherwise = False++ e <- runHaxl en $+ catchIf transientOrNotFound (throw (NotFound "haha")) $ \_ ->+ return True+ assertBool "catchIf1" e++ e <- runHaxl en $+ catchIf transientOrNotFound (throw (FetchError "haha")) $ \_ ->+ return True+ assertBool "catchIf2" e++ e <- runHaxl en $+ (catchIf transientOrNotFound (throw (CriticalError "haha")) $ \_ ->+ 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 + -- 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 () :: IO (Env () ())+ runHaxl en $ throw e `catch` \x -> return x printing :: Assertion printing = do@@ -73,7 +153,35 @@ BS.putStrLn $ encode c +withEnvTest :: Test+withEnvTest = TestLabel "withEnvTest" $ TestCase $ do+ exstate <- ExampleDataSource.initGlobalState+ e <- initEnv (stateSet exstate stateEmpty) False :: IO (Env Bool ())+ b <- runHaxl e $ withEnv e { userEnv = True } $ env userEnv+ assertBool "withEnv1" b+ e <- initEnv (stateSet exstate stateEmpty) False :: IO (Env Bool ())+ b <- runHaxl e $ withEnv e { userEnv = True } $ do+ _ <- countAardvarks "aaa"+ env userEnv+ assertBool "withEnv2" b+ e <- initEnv (stateSet exstate stateEmpty) False :: IO (Env Bool ())+ 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 :: IO (Env Bool ())+ 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,32 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE StandaloneDeriving, GADTs, DeriveDataTypeable #-}-module DataCacheTest (tests) where+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+module DataCacheTest (tests, newResult, takeResult) where import Haxl.Core.DataCache as DataCache-import Haxl.Core.Types+import Haxl.Core.Monad+import Haxl.Core import Control.Exception+import Data.Either import Data.Hashable import Data.Traversable import Data.Typeable import Prelude hiding (mapM) import Test.HUnit+import Data.IORef+import Data.Text+import Unsafe.Coerce data TestReq a where Req :: Int -> TestReq a -- polymorphic result@@ -21,42 +38,165 @@ instance Hashable (TestReq a) where hashWithSalt salt (Req i) = hashWithSalt salt i +instance DataSource u TestReq where+ fetch = error "no fetch defined" +instance DataSourceName TestReq where+ dataSourceName _ = pack "TestReq"++instance StateKey TestReq where+ data State TestReq = TestReqState++instance ShowP TestReq where showp = show++data CacheableReq x where CacheableInt :: Int -> CacheableReq Int+ deriving Typeable+deriving instance Eq (CacheableReq x)+deriving instance Show (CacheableReq x)+instance Hashable (CacheableReq x) where+ hashWithSalt s (CacheableInt val) = hashWithSalt s (0::Int, val)+++newResult :: Monoid w => a -> IO (IVar u w a)+newResult a = newFullIVar (Ok a mempty)++takeResult :: IVar u w a -> IO (ResultVal a w)+takeResult IVar{ivarRef = 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 m2 <- newResult "hello"- let cache =- DataCache.insert (Req 1 :: TestReq Int) m1 $- DataCache.insert (Req 2 :: TestReq String) m2 $- DataCache.empty+ cache <- emptyDataCache+ DataCache.insert (Req 2 :: TestReq String) m2 cache+ DataCache.insert (Req 1 :: TestReq Int) m1 cache -- "Req 1" has a result of type Int, so if we try to look it up -- with a result of type String, we should get Nothing, not a crash.- r <- mapM takeResult $ DataCache.lookup (Req 1) cache+ 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+ 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 Nothing) -> True _something_else -> False - r <- mapM takeResult $ DataCache.lookup (Req 2) cache+ 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" Nothing) -> True _something_else -> False - r <- mapM takeResult $ DataCache.lookup (Req 2) cache+ 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 +dcStrictnessTest :: Test+dcStrictnessTest = TestLabel "DataCache strictness" $ TestCase $ do+ env <- initEnv stateEmpty () :: IO (Env () ())+ r <- Control.Exception.try $ runHaxl env $+ cachedComputation (Req (error "BOOM")) $ return "OK"+ assertBool "dcStrictnessTest" $+ case r of+ Left (ErrorCall "BOOM") -> True+ _other -> False++dcFallbackTest :: Test+dcFallbackTest = TestLabel "DataCache fallback" $ TestList+ [ TestLabel "Base" $ TestCase $ do+ env <- mkEnv+ (r,cached) <- runHaxl env (do+ a <- dataFetch req+ b <- cacheResult (CacheableInt 1234) (return 99999)+ return (a,b))+ (Stats stats) <- readIORef (statsRef env)+ assertEqual "fallback still has stats" 1+ (Prelude.length [x | x@FetchStats{} <- stats])+ assertEqual "dcFallbackTest found" 1 r+ assertEqual "dcFallbackTest cached" 1234 cached+ , TestLabel "Exception" $ TestCase $ do+ env <- mkEnv+ rbad <- Control.Exception.try $ runHaxl env (dataFetch reqBad)+ assertBool "dcFallbackTest not found" $+ case rbad of+ Left (ErrorCall "no fetch defined") -> True+ _ -> False+ , TestLabel "Completions" $ TestCase $ do+ -- check applicative still runs as it would have without a fallback+ env <- mkEnv+ let+ fetchA = dataFetch reqEx+ fetchB = tellWrite 7 >> dataFetch req+ (rbad, writes) <- runHaxlWithWrites env $+ Haxl.Core.try $ (,) <$> fetchA <*> fetchB+ fetches <- countFetches env+ assertEqual "dispatched 2 fetches" 2 fetches+ assertBool "exception propogates" $+ case rbad of+ Left (NotFound _) -> True+ _ -> False+ assertEqual "write side effects happen" [7] (flattenWT writes)+ ]+ where++ mkEnv = addLookup <$> initEnv (stateSet TestReqState stateEmpty) ()++ countFetches env = do+ (Stats stats) <- readIORef (statsRef env)+ let+ c = sum [ fetchBatchSize x+ | x@FetchStats{} <- stats+ ]+ return c++ addLookup :: Env () (WriteTree Int) -> Env () (WriteTree Int)+ addLookup e = e { dataCacheFetchFallback = Just (DataCacheLookup lookup)+ , flags = (flags e) { report = profilingReportFlags }+ }+ lookup+ :: forall req a . Typeable (req a)+ => req a+ -> IO (Maybe (ResultVal a (WriteTree Int)))+ lookup r+ | typeOf r == typeRep (Proxy :: Proxy (TestReq Int)) =+ -- have to coerce on the way out as results are not Typeable+ -- so you better be sure you do it right!+ return $ unsafeCoerce . doReq <$> cast r+ | typeOf r == typeRep (Proxy :: Proxy (CacheableReq Int)) =+ return $ unsafeCoerce . doCache <$> cast r+ | otherwise = return Nothing++ doReq :: TestReq Int -> ResultVal Int (WriteTree Int)+ doReq (Req 999) = ThrowHaxl (toException $ NotFound empty) Nothing+ doReq (Req r) = Ok r Nothing++ doCache :: CacheableReq Int -> ResultVal Int (WriteTree Int)+ doCache (CacheableInt i) = Ok i Nothing++ req :: TestReq Int+ req = Req 1++ reqEx :: TestReq Int+ reqEx = Req 999++ reqBad :: TestReq String+ reqBad = Req 2++ -- tests :: Assertion-tests = TestList [dcSoundnessTest]+tests = TestList [ dcSoundnessTest+ , dcStrictnessTest+ , dcFallbackTest+ ]
+ tests/DataSourceDispatchTests.hs view
@@ -0,0 +1,83 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ApplicativeDo #-}++module DataSourceDispatchTests (tests) where+import Test.HUnit hiding (State)+import Control.Monad+import Haxl.Core+import Data.Hashable++data DataSourceDispatch ty where+ GetBatchSize :: Int -> DataSourceDispatch Int++deriving instance Eq (DataSourceDispatch ty)+deriving instance Show (DataSourceDispatch ty)++instance DataSourceName DataSourceDispatch where+ dataSourceName _ = "DataSourceDispatch"++instance StateKey DataSourceDispatch where+ data State DataSourceDispatch = DataSourceDispatchState++instance ShowP DataSourceDispatch where showp = show++instance Hashable (DataSourceDispatch a) where+ hashWithSalt s (GetBatchSize n) = hashWithSalt s n++initDataSource :: IO (State DataSourceDispatch)+initDataSource = return DataSourceDispatchState++instance DataSource UserEnv DataSourceDispatch where+ fetch _state _flags _u = SyncFetch $ \bfs -> forM_ bfs (fill $ length bfs)+ where+ fill :: Int -> BlockedFetch DataSourceDispatch -> IO ()+ fill l (BlockedFetch (GetBatchSize _ ) rv) = putResult rv (Right l)++ schedulerHint Batching = TryToBatch+ schedulerHint NoBatching = SubmitImmediately++data UserEnv = Batching | NoBatching deriving (Eq)++makeTestEnv :: UserEnv -> IO (Env UserEnv ())+makeTestEnv testUsrEnv = do+ st <- initDataSource+ e <- initEnv (stateSet st stateEmpty) testUsrEnv+ return e { flags = (flags e) {+ report = setReportFlag ReportFetchStats defaultReportFlags } }++schedulerTest:: Test+schedulerTest = TestCase $ do+ let+ fet = do+ x <- dataFetch (GetBatchSize 0)+ y <- dataFetch (GetBatchSize 1)+ return [x,y]++ e <- makeTestEnv Batching+ r1 :: [Int] <- runHaxl e fet+ assertEqual "Failed to create batches for data fetch" [2,2] r1++ eNoBatching <- makeTestEnv NoBatching+ r2 :: [Int] <- runHaxl eNoBatching fet+ assertEqual "Unexpexted batches in SubmitImmediately" [1,1] r2++ return ()++tests :: Test+tests = TestList+ [ TestLabel "schedulerTest" schedulerTest+ ]
tests/ExampleDataSource.hs view
@@ -1,3 +1,11 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -12,7 +20,7 @@ initGlobalState, -- * requests for this data source- ExampleReq(..),+ Id(..), ExampleReq(..), countAardvarks, listWombats, ) where@@ -24,6 +32,9 @@ import Data.Typeable import Data.Hashable+import Control.Concurrent+import qualified Control.Exception as E+import System.IO -- Here is an example minimal data source. Our data source will have -- two requests:@@ -63,7 +74,7 @@ deriving instance Show (ExampleReq a) -instance Show1 ExampleReq where show1 = show+instance ShowP ExampleReq where showp = show instance Hashable (ExampleReq a) where hashWithSalt s (CountAardvarks a) = hashWithSalt s (0::Int,a)@@ -87,7 +98,12 @@ -- I'll define exampleFetch below fetch = exampleFetch + -- we don't want to treat NotFound as an exception for stats purposes+ classifyFailure _ _ e+ | Just NotFound{} <- E.fromException e = IgnoredForStatsFailure+ | otherwise = StandardFailure + -- Every data source should define a function 'initGlobalState' that -- initialises the state for that data source. The arguments to this -- function might vary depending on the data source - we might need to@@ -115,10 +131,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@@ -134,18 +149,24 @@ putSuccess m 1 error "BANG2" -- the exception is propagated even if we have already -- put the result with putSuccess+fetch1 (BlockedFetch (CountAardvarks "BANG3") _) = do+ hPutStr stderr "BANG3"+ killThread =<< myThreadId -- an asynchronous exception+fetch1 (BlockedFetch (CountAardvarks "BANG4") r) = do+ putFailure r $ NotFound "BANG4" fetch1 (BlockedFetch (CountAardvarks str) m) = putSuccess m (length (filter (== 'a') str)) fetch1 (BlockedFetch (ListWombats a) r) =- if a > 99 then putFailure r $ FetchError "too large"- else putSuccess r $ take (fromIntegral a) [1..]+ if a > 999999+ then putFailure r $ FetchError "too large"+ else putSuccess r $ take (fromIntegral a) [1..] -- Normally a data source will provide some convenient wrappers for -- its requests: -countAardvarks :: String -> GenHaxl () Int+countAardvarks :: String -> GenHaxl u w Int countAardvarks str = dataFetch (CountAardvarks str) -listWombats :: Id -> GenHaxl () [Id]-listWombats id = dataFetch (ListWombats id)+listWombats :: Id -> GenHaxl u w [Id]+listWombats i = dataFetch (ListWombats i)
+ tests/ExceptionStackTests.hs view
@@ -0,0 +1,73 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module ExceptionStackTests (tests) where++import Prelude ()+import Haxl.Core+import Haxl.Prelude++import Test.HUnit++import qualified ExampleDataSource++testEnv :: ReportFlags -> IO (Env () ())+testEnv report = do+ exstate <- ExampleDataSource.initGlobalState+ let st = stateSet exstate stateEmpty+ env <- initEnv st ()+ return env{ flags = (flags env){ report = report } }++reportFlags :: ReportFlags+reportFlags = setReportFlag ReportExceptionLabelStack defaultReportFlags++runHaxlTest+ :: ReportFlags+ -> String+ -> (Int -> Int -> GenHaxl () () Int)+ -> IO (Maybe [Text])+runHaxlTest report str func = do+ env <- testEnv report+ result <- runHaxl env $+ withLabel "try" $ tryToHaxlException $ withLabel "test" $ do+ x <- withLabel "dummy" $ pure 1+ y <- withLabel "fetch" $ ExampleDataSource.countAardvarks str+ withLabel "func" $ func x y+ case result of+ Left (HaxlException stk _) -> return stk+ Right{} -> assertFailure "expected: HaxlException"++fetchException :: Test+fetchException = TestCase $ do+ result <- runHaxlTest reportFlags "BANG4" $ \i j -> return $ i + j+ assertEqual "stack" (Just ["fetch", "test", "try", "MAIN"]) result++userException :: Test+userException = TestCase $ do+ result <- runHaxlTest reportFlags "aaa" $ \_ _ -> withLabel "throw" $+ throw $ InvalidParameter "throw"+ assertEqual "stack" (Just ["throw", "func", "test", "try", "MAIN"]) result++#ifndef PROFILING+disabledExceptionStack :: Test+disabledExceptionStack = TestCase $ do+ result <- runHaxlTest defaultReportFlags "BANG4" $ \i j -> return $ i + j+ assertEqual "stack" Nothing result+#endif++tests :: Test+tests = TestList+ [ TestLabel "FetchException" fetchException+ , TestLabel "UserException" userException+#ifndef PROFILING+ , TestLabel "DisabledExceptionStack" disabledExceptionStack+#endif+ ]
+ tests/FullyAsyncTest.hs view
@@ -0,0 +1,67 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++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 :: IO (Env () ())+testEnv = do+ st <- mkConcurrentIOState+ env <- initEnv (stateSet st stateEmpty) ()+ return env { flags = (flags env) {+ report = setReportFlag ReportFetchStats defaultReportFlags } }++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)++{-+ 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,11 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE CPP, OverloadedStrings #-} module LoadCache where
tests/LoadCache.txt view
@@ -1,4 +1,4 @@-loadCache :: GenHaxl u ()+loadCache :: GenHaxl u w () loadCache = do cacheRequest (CountAardvarks "yyy") (except (LogicError (NotFound "yyy"))) cacheRequest (CountAardvarks "xxx") (Right (3))
− tests/Main.hs
@@ -1,20 +0,0 @@-{-# LANGUAGE RebindableSyntax, OverloadedStrings #-}-module Main where--import TestExampleDataSource-import BatchTests-import CoreTests-import DataCacheTest--import Data.String-import Test.HUnit--import Haxl.Prelude--main :: IO Counts-main = runTestTT $ TestList- [ TestLabel "ExampleDataSource" TestExampleDataSource.tests- , TestLabel "BatchTests" BatchTests.tests- , TestLabel "CoreTests" CoreTests.tests- , TestLabel "DataCacheTests" DataCacheTest.tests- ]
+ tests/MemoizationTests.hs view
@@ -0,0 +1,78 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++module MemoizationTests (tests) where++import Data.IORef++import Test.HUnit++import Haxl.Core+import Haxl.Core.Monad (unsafeLiftIO)++import ExampleDataSource++memoSoundness :: Test+memoSoundness = TestCase $ do+ iEnv <- do+ exState <- ExampleDataSource.initGlobalState+ initEnv (stateSet exState stateEmpty) () :: IO (Env () ())++ unMemoizedWombats <- runHaxl iEnv $ listWombats 100++ (initialGet, subsequentGet) <- runHaxl iEnv $ do+ wombatsMemo <- newMemoWith (listWombats 100)+ let memoizedWombats = runMemo wombatsMemo++ initialGet <- memoizedWombats+ subsequentGet <- memoizedWombats++ return (initialGet, subsequentGet)++ assertBool "Memo Soundness 1" $ initialGet == unMemoizedWombats+ assertBool "Memo Soundness 2" $ subsequentGet == unMemoizedWombats++ let impure runCounterRef = unsafeLiftIO $ do+ modifyIORef runCounterRef succ+ readIORef runCounterRef++ initialRunCounter = 0 :: Int++ runCounterRef <- newIORef initialRunCounter++ (initialImpureGet, subsequentImpureGet) <- runHaxl iEnv $ do+ impureMemo <- newMemoWith (impure runCounterRef)+ let memoizedImpure = runMemo impureMemo++ initialImpureGet <- memoizedImpure+ subsequentImpureGet <- memoizedImpure++ return (initialImpureGet, subsequentImpureGet)++ assertBool "Memo Soundness 3" $ initialImpureGet == succ initialRunCounter+ assertBool "Memo Soundness 4" $ subsequentImpureGet == initialImpureGet++ let fMemoVal = 42 :: Int++ dependentResult <- runHaxl iEnv $ do+ fMemoRef <- newMemo+ gMemoRef <- newMemo++ let f = runMemo fMemoRef+ g = runMemo gMemoRef++ prepareMemo fMemoRef $ return fMemoVal+ prepareMemo gMemoRef $ succ <$> f++ a <- f+ b <- g+ return (a + b)++ assertBool "Memo Soundness 5" $ dependentResult == fMemoVal + succ fMemoVal++tests = TestList [TestLabel "Memo Soundness" memoSoundness]
tests/MockTAO.hs view
@@ -1,3 +1,11 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -6,12 +14,14 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-} module MockTAO ( Id(..), initGlobalState, assocRangeId2s, friendsAssoc,+ friendsOf, ) where import Data.Hashable@@ -20,7 +30,11 @@ import Prelude () import qualified Data.Map as Map import qualified Data.Text as Text+import Control.Concurrent+import Control.Exception+import Control.Monad (void) + import Haxl.Prelude import Haxl.Core @@ -36,29 +50,34 @@ deriving instance Show (TAOReq a) deriving instance Eq (TAOReq a) -instance Show1 TAOReq where show1 = show+instance ShowP TAOReq where showp = show instance Hashable (TAOReq a) where 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 = BackgroundFetch $ \f -> do+ mask_ $ void . forkIO $ mapM_ (doFetch True) f+ | otherwise = SyncFetch $ mapM_ (doFetch False) -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) =- case Map.lookup (a, b) assocs of- Nothing -> putFailure r . NotFound . Text.pack $ show req- Just result -> putSuccess r result+doFetch :: Bool -> BlockedFetch TAOReq -> IO ()+doFetch bg (BlockedFetch req@(AssocRangeId2s a b) r) = put result+ where put = if bg then putResultFromChildThread r else putResult r+ result = case Map.lookup (a, b) assocs of+ Nothing -> except . NotFound . Text.pack $ show req+ Just result -> Right result + assocs :: Map (Id,Id) [Id] assocs = Map.fromList [ ((friendsAssoc, 1), [5..10]),@@ -72,3 +91,6 @@ assocRangeId2s :: Id -> Id -> Haxl [Id] assocRangeId2s a b = dataFetch (AssocRangeId2s a b)++friendsOf :: Id -> Haxl [Id]+friendsOf = assocRangeId2s friendsAssoc
+ tests/MonadAsyncTest.hs view
@@ -0,0 +1,130 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module MonadAsyncTest (tests) where+import Haxl.Core+import Test.HUnit hiding (State)+import Control.Concurrent+import Control.Exception as Exception+import Control.Monad+import Haxl.Core.Monad (unsafeLiftIO, WriteTree)+import System.IO.Unsafe+import Data.Hashable+import Data.IORef+import Data.Text (Text)++newtype SimpleWrite = SimpleWrite Text deriving (Eq, Show)++{-# NOINLINE shouldThrowRef #-}+shouldThrowRef :: IORef Bool+shouldThrowRef = unsafePerformIO (newIORef False)++-- | This datasource contains "bad" code which can throw at the wrong+-- moment.+data ThrowableSleep a where+ Sleep :: Int -> ThrowableSleep Int++deriving instance Eq (ThrowableSleep a)+deriving instance Show (ThrowableSleep a)++instance ShowP ThrowableSleep where showp = show++instance Hashable (ThrowableSleep a) where+ hashWithSalt s (Sleep n) = hashWithSalt s n++instance StateKey ThrowableSleep where+ data State ThrowableSleep = ThrowableSleepState++initDataSource :: IO (State ThrowableSleep)+initDataSource = return ThrowableSleepState++instance DataSourceName ThrowableSleep where+ dataSourceName _ = "ThrowableSleep"++instance DataSource u ThrowableSleep where+ fetch _state _flags _u = BackgroundFetch $ \bfs -> forM_ bfs fill+ where+ fill :: BlockedFetch ThrowableSleep -> IO ()+ fill (BlockedFetch (Sleep n) rv) = do+ _ <- forkFinally+ (do+ threadDelay (n*1000)+ return n+ )+ (\res -> do+ shouldThrow <- atomicModifyIORef' shouldThrowRef (\s -> (False, s))+ -- Simulate case when datasource throws before putting Result into+ -- completions queue.+ when shouldThrow $ do+ throwIO $ ErrorCall "datasource threw an exception"+ -- In case the datasource throws before this point, there'll be+ -- nothing to put the result to the queue of 'completions', and+ -- therefore Haxl would block indefinitely.+ --+ -- Note that Haxl tries to catch datasource exceptions and put the+ -- "exception result" into `completions` using `wrapFetchInCatch`+ -- function. However that doesn't work in this case because the+ -- datasource throws in a separate thread.+ putResultFromChildThread rv res+ )+ return ()++tests :: Test+tests = TestList+ [ TestLabel "exceptionTest" exceptionTest+ ]++mkTestEnv :: IO (Env () (WriteTree SimpleWrite))+mkTestEnv = do+ st <- initDataSource+ initEnv (stateSet st stateEmpty) ()++exceptionTest :: Test+exceptionTest = TestCase $ do+ e <- mkTestEnv++ let+ fet (n :: Int) (st :: Bool )= do+ x <- dataFetch (Sleep (fromIntegral n))+ unsafeLiftIO $ writeIORef shouldThrowRef st+ y <- dataFetch (Sleep (fromIntegral x*2))+ return (x+y)++ r1 :: (Either Exception.SomeException Int)+ <- Exception.try $ runHaxl e $ fet 10 True++ -- Datasources are responsible for putting the fetched result into the+ -- completions queue. If for some reason they fail to do so, Haxl throws a+ -- LogicBug since the scheduler is still expecting some request(s) to+ -- be completed.+ case r1 of+ Left ex | Just (LogicBug _) <- Exception.fromException ex -> return ()+ _ -> assertFailure "r1 computation did not fail with Logic Bug!"++ -- Sanitize the env to get rid of all empty IVars+ -- While this test examines the case when there's an exception in the Haxl+ -- datasource itself, a similar behavior will occur in case an async+ -- exception is thrown to the Haxl scheduler thread.+ e' <- sanitizeEnv e++ r2 :: (Either Exception.SomeException Int)+ <- Exception.try $ runHaxl e' $ fet 10 False+ case r2 of+ Right _ -> return ()+ Left ex | Just (LogicBug _) <- Exception.fromException ex -> do+ assertFailure $ "bad exception in r2: " ++ show ex+ Left _ -> return ()
+ tests/MonadBench.hs view
@@ -0,0 +1,173 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++-- | Benchmarking tool for core performance characteristics of the Haxl monad.+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ApplicativeDo, RecordWildCards #-}+module MonadBench (main) where++import Control.Monad+import Data.List as List+import Data.Maybe+import Data.Time.Clock+import Options.Applicative+import System.Exit+import System.IO+import Text.Printf++import Haxl.Prelude as Haxl+import Prelude()++import Haxl.Core+import Haxl.Core.Monad (WriteTree)+import Haxl.Core.Util++import ExampleDataSource++newtype SimpleWrite = SimpleWrite Text deriving (Eq, Show)++testEnv :: ReportFlags -> IO (Env () (WriteTree SimpleWrite))+testEnv report = do+ exstate <- ExampleDataSource.initGlobalState+ let st = stateSet exstate stateEmpty+ env <- initEnv st ()+ return env { flags = (flags env) { report = report } }++type Test = (String, Int, Int -> GenHaxl () (WriteTree SimpleWrite) ())++testName :: Test -> String+testName (t,_,_) = t++allTests :: [Test]+allTests =+ -- parallel, identical queries+ [ ("par1", large, \n -> Haxl.sequence_ (replicate n (listWombats 3)))+ -- parallel, distinct queries+ , ("par2", medium, \n ->+ Haxl.sequence_ (map listWombats [1..fromIntegral n]))+ -- sequential, identical queries+ , ("seqr", huge, \n ->+ foldr andThen (return ()) (replicate n (listWombats 3)))+ -- sequential, left-associated, distinct queries+ , ("seql", medium, \n -> do+ _ <- foldl andThen (return []) (map listWombats [1.. fromIntegral n])+ return ())+ -- No memoization+ , ("memo0", small, \n -> Haxl.sequence_ [unionWombats | _ <- [1..n]])+ -- One put, N gets.+ , ("memo1", medium_large, \n ->+ Haxl.sequence_ [memo (42 :: Int) unionWombats | _ <- [1..n]])+ -- N puts, N gets.+ , ("memo2", small, \n ->+ Haxl.sequence_ [memo (i :: Int) unionWombats | i <- [1..n]])+ , ("memo3", medium_large, \n -> do+ ref <- newMemoWith unionWombats+ let c = runMemo ref+ Haxl.sequence_ [c | _ <- [1..n]])+ , ("memo4", small, \n -> do+ let f = unionWombatsTo+ Haxl.sequence_ [f x | x <- take n $ cycle [100, 200 .. 1000]])+ , ("memo5", medium_large, \n -> do+ f <- memoize1 unionWombatsTo+ Haxl.sequence_ [f x | x <- take n $ cycle [100, 200 .. 1000]])+ , ("memo6", small, \n -> do+ let f = unionWombatsFromTo+ Haxl.sequence_ [ f x y+ | x <- take n $ cycle [100, 200 .. 1000]+ , let y = x + 1000+ ])+ , ("memo7", medium_large, \n -> do+ f <- memoize2 unionWombatsFromTo+ Haxl.sequence_ [ f x y+ | x <- take n $ cycle [100, 200 .. 1000]+ , let y = x + 1000+ ])+ , ("cc1", medium_large, \n ->+ Haxl.sequence_ [ cachedComputation (ListWombats 1000) unionWombats+ | _ <- [1..n]+ ])+ , ("tree", 20, \n -> void $ tree n (\_ act -> act))+ , ("tree_labels", 20, \n -> void $+ tree n (\n act -> withLabel (textShow n) act))+ -- parallel writes+ , ("write1", large, \n ->+ Haxl.sequence_ (replicate n (tellWrite (SimpleWrite "haha"))))+ -- sequential writes+ , ("write2", huge, \n -> foldr+ andThen+ (return ())+ (replicate n (tellWrite (SimpleWrite "haha"))))+ ]+ where+ huge = large * 10+ large = medium * 10+ medium_large = medium * 4+ medium = 200000+ small = 1000++data Options = Options+ { test :: String+ , nOverride :: Maybe Int+ , reportFlag :: ReportFlags+ }++runTest :: Options -> Test -> IO ()+runTest Options{..} (t, nDef, act) = do+ let n = fromMaybe nDef nOverride+ env <- testEnv reportFlag+ t0 <- getCurrentTime+ runHaxl env $ act n+ t1 <- getCurrentTime+ printf "%12s: %10d reqs: %.2fs\n"+ t n (realToFrac (t1 `diffUTCTime` t0) :: Double)++optionsParser :: Parser Options+optionsParser = do+ test <- argument str (metavar "TEST")+ reportFlag <- reportFlagParser+ nOverride <- optional $ argument auto (metavar "NUM")+ return Options{..}+ where+ reportFlagParser = foldl' (flip ($)) defaultReportFlags <$> sequenceA+ [ flag id (setReportFlag i) $ long $ show i+ | i <- enumFrom minBound+ ]++main :: IO ()+main = do+ opts@Options{..} <- execParser $ info optionsParser mempty+ let tests = if test == "all"+ then allTests+ else filter ((==) test . testName) allTests+ when+ (null tests)+ (do+ hPutStrLn stderr $ "syntax: monadbench [all|" +++ intercalate "|" (map testName allTests) ++ "]"+ exitWith (ExitFailure 1))+ Control.Monad.mapM_ (runTest opts) tests++tree+ :: Int+ -> (Int -> GenHaxl () (WriteTree SimpleWrite) [Id]+ -> GenHaxl () (WriteTree SimpleWrite) [Id])+ -> GenHaxl () (WriteTree SimpleWrite) [Id]+tree 0 wrap = wrap 0 $ listWombats 0+tree n wrap = wrap n $ concat <$> Haxl.sequence+ [ tree (n-1) wrap+ , listWombats (fromIntegral n), tree (n-1) wrap+ ]++unionWombats :: GenHaxl () (WriteTree SimpleWrite) [Id]+unionWombats = foldl List.union [] <$> Haxl.mapM listWombats [1..1000]++unionWombatsTo :: Id -> GenHaxl () (WriteTree SimpleWrite) [Id]+unionWombatsTo x = foldl List.union [] <$> Haxl.mapM listWombats [1..x]++unionWombatsFromTo :: Id -> Id -> GenHaxl () (WriteTree SimpleWrite) [Id]+unionWombatsFromTo x y = foldl List.union [] <$> Haxl.mapM listWombats [x..y]
+ tests/OutgoneFetchesTests.hs view
@@ -0,0 +1,82 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ApplicativeDo #-}+module OutgoneFetchesTests (tests) where++import Haxl.Prelude as Haxl+import Prelude()++import Haxl.Core+import Haxl.DataSource.ConcurrentIO++import Data.IORef+import qualified Data.Map as Map+import Data.Typeable+import Test.HUnit+import System.Timeout++import ExampleDataSource+import SleepDataSource++testEnv :: IO (Env () ())+testEnv = do+ exstate <- ExampleDataSource.initGlobalState+ sleepState <- mkConcurrentIOState+ let st = stateSet exstate $ stateSet sleepState stateEmpty+ e <- initEnv st ()+ return e { flags = (flags e) {+ report = setReportFlag ReportOutgoneFetches defaultReportFlags } }+ -- report=1 to enable fetches tracking++-- A cheap haxl computation we interleave b/w the @sleep@ fetches.+wombats :: GenHaxl () () Int+wombats = length <$> listWombats 3++outgoneFetchesTest :: String -> Int -> GenHaxl () () a -> Test+outgoneFetchesTest label unfinished haxl = TestLabel label $ TestCase $ do+ env <- testEnv+ _ <- timeout (100*1000) $ runHaxl env haxl -- 100ms+ actual <- getMapFromRCMap <$> readIORef (submittedReqsRef env)+ assertEqual "fetchesMap" expected actual+ where+ expected = if unfinished == 0 then Map.empty else+ Map.singleton (dataSourceName (Proxy :: Proxy (ConcurrentIOReq Sleep))) $+ Map.singleton (typeOf1 (undefined :: ConcurrentIOReq Sleep a)) unfinished++tests :: Test+tests = TestList+ [ outgoneFetchesTest "finished" 0 $ do+ -- test that a completed datasource fetch doesn't show up in Env+ _ <- sleep 1 -- finished+ _ <- sleep 1 -- cached/finished+ _ <- sleep 1 -- cached/finished+ wombats+ , outgoneFetchesTest "unfinished" 2 $ do+ -- test that unfinished datasource fetches shows up in Env+ _ <- sleep 200 -- unfinished+ _ <- wombats+ _ <- sleep 300 -- unfinished+ _ <- wombats+ return ()+ , outgoneFetchesTest "mixed" 2 $ do+ -- test for finished/unfinished fetches from the same datasource+ _ <- sleep 1 -- finished+ _ <- sleep 200 -- unfinished+ _ <- sleep 300 -- unfinished+ return ()+ , outgoneFetchesTest "cached" 1 $ do+ -- test for cached requests not showing up twice in ReqCountMap+ _ <- sleep 200 -- unfinished+ _ <- sleep 200 -- cached/unfinished+ return ()+ , outgoneFetchesTest "unsent" 1 $+ -- test for unsent requests not showing up in ReqCountMap+ sleep 200 `andThen` sleep 300 -- second req should never be sent+ ]
+ tests/ParallelTests.hs view
@@ -0,0 +1,81 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++module ParallelTests where++import Haxl.Prelude+import Haxl.Core++import Haxl.DataSource.ConcurrentIO+import SleepDataSource++import Data.Time.Clock++import Test.HUnit++testEnv :: IO (Env () ())+testEnv = do+ sleepState <- mkConcurrentIOState+ let st = stateSet sleepState stateEmpty+ initEnv st ()++sync_test :: IO ()+sync_test = do+ env <- testEnv+ -- This computation tests that the two arguments of the pOr can fire+ -- without causing an error. The reason we test for this is that the+ -- synchronization involved in this case is a little fragile.+ False <- runHaxl env $ do+ (fmap (const False) (sleep 50)+ `pOr` fmap (const False) (sleep 100))+ `pOr` fmap (const False) (sleep 200)+ return ()++semantics_when_computation_is_blocked_test :: IO ()+semantics_when_computation_is_blocked_test = do+ env <- testEnv+ -- Test semantics of blocking+ let sleepReturn bool t = do+ _ <- sleep t+ return bool+ r <- runHaxl env $ do+ -- All sleep times are different so that they're not cached+ a <- sleepReturn False 10 `pOr` sleepReturn False 11+ b <- sleepReturn False 12 `pOr` sleepReturn True 13+ c <- sleepReturn True 14 `pOr` sleepReturn False 15+ d <- sleepReturn True 16 `pOr` sleepReturn True 17+ return (not a && b && c && d)+ assertBool "pOr blocked semantics" r+++timing_test = do+ env <- testEnv+ t0 <- getCurrentTime+ True <- runHaxl env $+ fmap (const True) (sleep 200) `pOr` fmap (const True) (sleep 100)+ t1 <- getCurrentTime+ True <- runHaxl env $+ fmap (const True) (sleep 100) `pOr` fmap (const True) (sleep 200)+ t2 <- getCurrentTime+ False <- runHaxl env $+ fmap (const False) (sleep 200) `pOr` fmap (const False) (sleep 100)+ t3 <- getCurrentTime+ False <- runHaxl env $+ fmap (const False) (sleep 100) `pOr` fmap (const False) (sleep 200)+ t4 <- getCurrentTime+ -- diffUTCTime returns the difference in seconds,+ -- while sleep expects milliseconds+ assert (t4 `diffUTCTime` t3 < 0.2)+ assert (t3 `diffUTCTime` t2 < 0.2)+ assert (t2 `diffUTCTime` t1 < 0.2)+ assert (t1 `diffUTCTime` t0 < 0.2)++tests = TestList [TestLabel "sync_test" (TestCase sync_test)+ ,TestLabel "timing_test" (TestCase timing_test)+ ,TestLabel "semantics_when_computation_is_blocked_test" (TestCase semantics_when_computation_is_blocked_test)+ ]
+ tests/ProfileTests.hs view
@@ -0,0 +1,218 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP #-}++module ProfileTests where++import Haxl.Prelude++import Haxl.Core+import Haxl.Core.Monad+import Haxl.Core.Stats++import Test.HUnit++import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Data.Aeson+import Data.IORef+import qualified Data.HashMap.Strict as HashMap+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap as KeyMap+#else+import qualified Data.HashMap.Strict as KeyMap+#endif+import Data.Int++import TestTypes+import TestUtils+import WorkDataSource+import SleepDataSource++mkProfilingEnv :: IO HaxlEnv+mkProfilingEnv = do+ env <- makeTestEnv False+ return env { flags = (flags env) { report = profilingReportFlags } }++-- expects only one label to be shown+labelToDataMap :: Profile -> HashMap.HashMap ProfileLabel ProfileData+labelToDataMap Profile{..} = HashMap.fromList hashKeys+ where+ labelKeys = HashMap.fromList [+ (k, l) | ((l, _), k) <- HashMap.toList profileTree]+ hashKeys = [ (l, v)+ | (k, v) <- HashMap.toList profile+ , Just l <- [HashMap.lookup k labelKeys]]++collectsdata :: Assertion+collectsdata = do+ e <- mkProfilingEnv+ _x <- runHaxl e $+ withLabel "bar" $+ withLabel "foo" $ do+ u <- env userEnv+ slp <- sum <$> mapM (\x -> withLabel "baz" $ return x) [1..5]+ -- do some non-trivial work that can't be lifted out+ -- first sleep though in order to force a Blocked result+ sleep slp `andThen` case fromJSON <$> KeyMap.lookup "A" u of+ Just (Success n) | sum [n .. 1000::Integer] > 0 -> return 5+ _otherwise -> return (4::Int)+ profCopy <- readIORef (profRef e)+ let+ profData = profile profCopy+ labelKeys = HashMap.fromList [+ (l, k) | ((l, _), k) <- HashMap.toList (profileTree profCopy)]+ getData k = do+ k2 <- HashMap.lookup k labelKeys+ HashMap.lookup k2 profData+ assertEqual "has data" 4 $ HashMap.size profData+ assertBool "foo allocates" $+ case profileAllocs <$> getData "foo" of+ Just x -> x > 10000+ Nothing -> False+ assertEqual "foo is only called once" (Just 1) $+ profileLabelHits <$> getData "foo"+ assertEqual "baz is called 5 times" (Just 5) $+ profileLabelHits <$> getData "baz"+ assertBool "bar does not allocate (much)" $+ case profileAllocs <$> getData "bar" of+ Just n -> n < 5000 -- getAllocationCounter can be off by +/- 4K+ _otherwise -> False+ let fooParents = case HashMap.lookup "foo" labelKeys of+ Nothing -> []+ Just kfoo ->+ [ kparent+ | ((_, kparent), k) <- HashMap.toList (profileTree profCopy)+ , k == kfoo]+ assertEqual "foo's parent" 1 (length fooParents)+ assertEqual "foo's parent is bar" (Just (head fooParents)) $+ HashMap.lookup ("bar", 0) (profileTree profCopy)+++collectsLazyData :: Assertion+collectsLazyData = do+ e <- mkProfilingEnv+ _x <- runHaxl e $ withLabel "bar" $ do+ u <- env userEnv+ withLabel "foo" $ do+ let start = if KeyMap.member "A" u+ then 10+ else 1+ return $ sum [start..10000::Integer]+ profCopy <- readIORef (profRef e)+ -- check the allocations are attributed to foo+ assertBool "foo has allocations" $+ case profileAllocs <$> HashMap.lookup "foo" (labelToDataMap profCopy) of+ Just x -> x > 10000+ Nothing -> False++exceptions :: Assertion+exceptions = do+ env <- mkProfilingEnv+ _x <- runHaxl env $+ withLabel "outer" $+ tryToHaxlException $ withLabel "inner" $+ unsafeLiftIO $ evaluate $ force (error "pure exception" :: Int)+ profData <- labelToDataMap <$> readIORef (profRef env)+ assertBool "inner label not added" $+ not $ HashMap.member "inner" profData++ env2 <- mkProfilingEnv+ _x <- runHaxl env2 $+ withLabel "outer" $+ tryToHaxlException $ withLabel "inner" $+ throw $ NotFound "haxl exception"+ profData <- labelToDataMap <$> readIORef (profRef env2)+ 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 :: Integer -> Assertion+threadAlloc batches = do+ env' <- initEnv (stateSet mkWorkState stateEmpty) () :: IO (Env () ())+ let env = env' { flags = (flags env') {+ report = setReportFlag ReportFetchStats defaultReportFlags } }+ a0 <- getAllocationCounter+ let+ wsize = 100000+ w = forM [wsize..(wsize+batches-1)] work+ _x <- runHaxl env $ sum <$> w+ a1 <- getAllocationCounter+ let+ lower = fromIntegral $ 1000000 * batches+ upper = fromIntegral $ 25000000 * batches+ assertBool "threadAlloc lower bound" $ (a0 - a1) > lower+ assertBool "threadAlloc upper bound" $ (a0 - a1) < upper+ -- the result was 16MB on 64-bit, or around 25KB if we miss the allocs+ -- in the child thread. For batched it should be similarly scaled.+ -- When we do not reset the counter for each batch was+ -- scaled again by number of batches.++ stats <- readIORef (statsRef env)+ assertEqual+ "threadAlloc: batches"+ [fromIntegral batches]+ (aggregateFetchBatches length stats)+ -- if we actually do more than 1 batch then the above test is not useful++data MemoType = Global | Local++-- Test that we correctly attribute memo work+memos:: MemoType -> Assertion+memos memoType = do+ env <- mkProfilingEnv+ let+ memoAllocs = 10000000 :: Int64+ doWork = unsafeLiftIO $ do+ a0 <- getAllocationCounter+ setAllocationCounter $ a0 - memoAllocs+ return (5 :: Int)+ mkWork+ | Global <- memoType = return (memo (1 :: Int) doWork)+ | Local <- memoType = memoize doWork+ _ <- runHaxl env $ do+ work <- mkWork+ andThen+ (withLabel "do" work)+ (withLabel "cached" work)+ profData <- labelToDataMap <$> readIORef (profRef env)+ case HashMap.lookup "do" profData of+ Nothing -> assertFailure "do not in data"+ Just ProfileData{..} -> do+ assertEqual "has correct memo id" profileMemos [ProfileMemo 1 False]+ assertBool "allocs are included in 'do'" (profileAllocs >= memoAllocs)+ case HashMap.lookup "cached" profData of+ Nothing -> assertFailure "cached not in data"+ Just ProfileData{..} -> do+ assertEqual "has correct memo id" profileMemos [ProfileMemo 1 True]+ assertBool "allocs are *not* included in 'cached'" (profileAllocs < 50000)+ (Stats memoStats) <- readIORef (statsRef env)+ assertEqual "exactly 1 memo/fetch" 1 (length memoStats)+ let memoStat = head memoStats+ putStrLn $ "memoStat=" ++ show memoStat+ assertEqual "correct call id" 1 (memoStatId memoStat)+ assertBool "allocs are big enough" $ memoSpace memoStat >= memoAllocs+ assertBool "allocs are not too big" $ memoSpace memoStat < memoAllocs + 100000+++tests = TestList+ [ TestLabel "collectsdata" $ TestCase collectsdata+ , TestLabel "collectsdata - lazy" $ TestCase collectsLazyData+ , TestLabel "exceptions" $ TestCase exceptions+ , TestLabel "threads" $ TestCase (threadAlloc 1)+ , TestLabel "threads with batch" $ TestCase (threadAlloc 50)+ , TestLabel "memos - Global" $ TestCase (memos Global)+ , TestLabel "memos - Local" $ TestCase (memos Local)+ ]
+ tests/SleepDataSource.hs view
@@ -0,0 +1,48 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE FlexibleInstances #-}++module SleepDataSource (+ Sleep, 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 w 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/StatsTests.hs view
@@ -0,0 +1,195 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}++module StatsTests (tests) where++import Test.HUnit+import Data.List+import Data.Maybe++import Haxl.Prelude+import Haxl.Core+import Prelude()++import ExampleDataSource+import SleepDataSource+import Haxl.DataSource.ConcurrentIO++import Control.Monad (void)+import Data.IORef+import qualified Data.HashMap.Strict as HashMap++aggregateBatches :: Test+aggregateBatches = TestCase $ do+ let+ statsNoBatches = [ FetchStats { fetchDataSource = "foo"+ , fetchBatchSize = 7+ , fetchStart = 0+ , fetchDuration = 10+ , fetchSpace = 1+ , fetchFailures = 2+ , fetchIgnoredFailures = 0+ , fetchBatchId = n+ , fetchIds = [1,2] }+ | n <- reverse [1..10] ++ [11..20] ]+ ++ [ FetchCall "A" ["B"] 1, FetchCall "C" ["D"] 2 ]+ fetchBatch = [ FetchStats { fetchDataSource = "batch"+ , fetchBatchSize = 1+ , fetchStart = 100+ , fetchDuration = 1000 * n+ , fetchSpace = 3+ , fetchFailures = if n <= 3 then 1 else 0+ , fetchIgnoredFailures = 0+ , fetchBatchId = 123+ , fetchIds = [fromIntegral n] } | n <- [1..50] ]+ agg (sz,bids) FetchStats{..} = (sz + fetchBatchSize, fetchBatchId:bids)+ agg _ _ = error "unexpected"+ agg' = foldl' agg (0,[])+ aggNoBatch = aggregateFetchBatches agg' (Stats statsNoBatches)+ expectedNoBatch = [(7, [n]) | n <- reverse [1..20] :: [Int]]+ aggBatch = aggregateFetchBatches agg' (Stats fetchBatch)+ expectedResultBatch = (50, [123 | _ <- [1..50] :: [Int]])+ aggInterspersedBatch =+ aggregateFetchBatches agg'+ (Stats $ intersperse (head fetchBatch) statsNoBatches)+ expectedResultInterspersed =+ (21, [123 | _ <- [1..21] :: [Int]]) : expectedNoBatch+ assertEqual "No batch has no change" expectedNoBatch aggNoBatch+ assertEqual "Batch is combined" [expectedResultBatch] aggBatch+ assertEqual+ "Grouping works as expected" expectedResultInterspersed aggInterspersedBatch++testEnv :: IO (Env () ())+testEnv = do+ -- To use a data source, we need to initialize its state:+ exstate <- ExampleDataSource.initGlobalState+ sleepState <- mkConcurrentIOState++ -- And create a StateStore object containing the states we need:+ let st = stateSet exstate (stateSet sleepState stateEmpty)++ -- Create the Env:+ env <- initEnv st ()+ return env{ flags = (flags env){+ report = setReportFlag ReportFetchStack profilingReportFlags } }+++fetchIdsSync :: Test+fetchIdsSync = TestCase $ do+ env <- testEnv+ _ <- runHaxl env $+ sequence_+ [ void $ countAardvarks "abcabc" + (length <$> listWombats 3)+ , void $ listWombats 100+ , void $ listWombats 99+ , void $ countAardvarks "BANG4" `catch` \NotFound{} -> return 123+ ]+ -- expect a single DS stat+ (Stats stats) <- readIORef (statsRef env)+ let+ fetchStats = [x | x@FetchStats{} <- stats]+ assertEqual "Only 1 batch" 1 (length fetchStats)+ let+ [stat] = fetchStats+ assertEqual "No real failures" 0 (fetchFailures stat)+ assertEqual "1 ignored failure" 1 (fetchIgnoredFailures stat)++fetchIdsBackground :: Test+fetchIdsBackground = TestCase $ do+ env <- testEnv+ _ <- runHaxl env $+ sequence_+ [ withLabel "short" $ sleep 1+ , withLabel "long" $ sleep 500 ]++ -- make sure that with memo'ing we still preserve the stack+ _ <- runHaxl env $ withLabel "base"+ (memo (1 :: Int) $ withLabel "child" $ sleep 102)++ _ <- runHaxl env $ withLabel "short_cached" $ sleep 1++ -- expect a single DS stat+ (Stats stats) <- readIORef (statsRef env)+ (Profile p pt _) <- readIORef (profRef env)+ let+ keyMap =+ HashMap.fromList [ (label, k) | ((label,_), k) <- HashMap.toList pt]+ revMap = HashMap.fromList [(v,k) | (k,v) <- HashMap.toList pt]+ parentMap =+ HashMap.fromList $+ catMaybes+ [ case HashMap.lookup kp revMap of+ Just (lp,_) -> Just (label, lp)+ Nothing -> Nothing+ | ((label,kp), _) <- HashMap.toList pt]+ fetchMap = HashMap.fromList [ (fid, x) | x@FetchStats{} <- stats+ , fid <- fetchIds x]+ get l = [ (prof, wasCached, fetchStat)+ | Just key <- [HashMap.lookup l keyMap]+ , Just prof <- [HashMap.lookup key p]+ , ProfileFetch fid _ wasCached <- profileFetches prof+ , Just fetchStat <- [HashMap.lookup fid fetchMap]]+ [(short, shortWC, shortFetch)] = get "short"+ [(long, longWC, longFetch)] = get "long"+ [(shortCached, shortCachedWC, shortCachedFetch)] = get "short_cached"++ assertEqual "3 batches" 3 (HashMap.size fetchMap)+ assertEqual "6 labels (inc MAIN)" 6 (HashMap.size keyMap)++ assertEqual "child parent is base"+ (Just "base")+ (HashMap.lookup "child" parentMap)++ assertEqual "base parent is MAIN"+ (Just "MAIN")+ (HashMap.lookup "base" parentMap)++ assertEqual "long parent is MAIN"+ (Just "MAIN")+ (HashMap.lookup "long" parentMap)++ assertBool "original fetches not cached (short)" (not shortWC)+ assertBool "original fetches not cached (long)" (not longWC)+ assertBool "was cached short" shortCachedWC++ assertEqual "one fetch short" 1 (length $ profileFetches short)+ assertEqual "one fetch long" 1 (length $ profileFetches long)+ assertEqual "one fetch short_cached" 1 (length $ profileFetches shortCached)++ assertBool "short fetch mapped properly" (fetchDuration shortFetch < 100000)+ assertEqual+ "short cached fetch mapped properly"+ (fetchDuration shortFetch)+ (fetchDuration shortCachedFetch)+ assertBool "long fetch was mapped properly" (fetchDuration longFetch > 100000)+++ppStatsTest :: Test+ppStatsTest = TestCase $ do+ let+ r = ppStats (Stats [])+ mc = ppStats (Stats [MemoCall 0 0])+ fc = ppStats (Stats [FetchCall "" [] 0])+ fw = ppStats (Stats [FetchWait HashMap.empty 0 1])+ fs = ppStats (Stats [FetchStats "" 0 0 0 0 0 0 0 []])+ assertEqual "empty stats -> empty string" r ""+ assertEqual "memo call stats -> empty string" mc ""+ assertEqual "fetch call stats -> empty string" fc ""+ assertBool "fetch wait stats -> some data" (not $ null fw)+ assertBool "fetch stats -> some data" (not $ null fs)+++tests = TestList [ TestLabel "Aggregate Batches" aggregateBatches+ , TestLabel "Fetch IDs Sync" fetchIdsSync+ , TestLabel "Fetch IDs Background" fetchIdsBackground+ , TestLabel "ppStats" ppStatsTest ]
+ tests/TestBadDataSource.hs view
@@ -0,0 +1,130 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+module TestBadDataSource (tests) where++import Haxl.Prelude as Haxl+import Prelude()++import Haxl.Core++import Data.IORef+import Test.HUnit+import Control.Exception+import System.Mem++import ExampleDataSource+import BadDataSource++testEnv impl fn = do+ -- Use allocation limits, just to make sure haxl properly behaves and+ -- doesn't reset this internally somewhere.+ -- `go` will disable this+ setAllocationCounter 5000000 -- 5 meg should be enough, uses ~100k atm+ enableAllocationLimit+ exstate <- ExampleDataSource.initGlobalState+ badstate <- BadDataSource.initGlobalState impl+ let st = stateSet exstate $ stateSet (fn badstate) stateEmpty+ initEnv st ()++wombats :: GenHaxl () () Int+wombats = length <$> listWombats 3++wombatsMany :: GenHaxl () () Int+wombatsMany = length <$> listWombats 7++go :: FetchImpl -> Test+go impl = TestCase $ flip finally disableAllocationLimit $ do+ -- test that a failed acquire doesn't fail the other requests+ ref <- newIORef False+ env <- testEnv impl $ \st ->+ st { failAcquire = throwIO (DataSourceError "acquire")+ , failRelease = writeIORef ref True }++ x <- runHaxl env $+ (dataFetch (FailAfter 0) + wombatsMany)+ `Haxl.catch` \DataSourceError{} -> wombats++ assertEqual "badDataSourceTest1" 3 x++ -- We should *not* have called release+ assertEqual "badDataSourceTest2" False =<< readIORef ref++ -- test that a failed dispatch doesn't fail the other requests+ ref <- newIORef False+ env <- testEnv impl $ \st ->+ st { failDispatch = throwIO (DataSourceError "dispatch")+ , failRelease = writeIORef ref True }++ x <- runHaxl env $+ (dataFetch (FailAfter 0) + wombatsMany)+ `Haxl.catch` \DataSourceError{} -> wombats++ assertEqual "badDataSourceTest3" x 3++ -- We *should* have called release+ assertEqual "badDataSourceTest4" True =<< readIORef ref++ -- test that a failed wait is a DataSourceError+ env <- testEnv impl $ \st ->+ st { failWait = throwIO (DataSourceError "wait") }++ x <- runHaxl env $+ (dataFetch (FailAfter 0) + wombatsMany)+ `Haxl.catch` \DataSourceError{} -> wombats++ assertEqual "badDataSourceTest5" x 3++ -- We *should* have called release+ assertEqual "badDataSourceTest6" True =<< readIORef ref++ -- test that a failed release is still a DataSourceError, even+ -- though the request will have completed successfully+ env <- testEnv impl $ \st ->+ st { failRelease = throwIO (DataSourceError "release") }++ let+ -- In background fetches the scheduler might happen to process the data+ -- source result (FetchError in this case) before it processes the exception+ -- from release. So we have to allow both cases.+ isBg = case impl of+ Background -> True+ BackgroundMVar -> True+ _ -> False+ releaseCatcher e+ | Just DataSourceError{} <- fromException e = wombats+ | Just FetchError{} <- fromException e =+ if isBg then wombats else Haxl.throw e+ | otherwise = Haxl.throw e++ x <- runHaxl env $+ (dataFetch (FailAfter 0) + wombatsMany)+ `Haxl.catch` releaseCatcher++ assertEqual "badDataSourceTest7" x 3++ -- test that if we don't throw anything we get the result+ -- (which is a fetch error for this source)+ env <- testEnv impl id+ x <- runHaxl env $+ (dataFetch (FailAfter 0) + wombatsMany)+ `Haxl.catch` \FetchError{} -> wombats++ assertEqual "badDataSourceTest8" x 3+++++tests = TestList+ [ TestLabel "badDataSourceTest async" (go Async)+ , TestLabel "badDataSourceTest background" (go Background)+ , TestLabel "badDataSourceTest backgroundMVar" (go BackgroundMVar)+ , TestLabel "badDataSourceTest backgroundFetchSeq" (go BackgroundSeq)+ , TestLabel "badDataSourceTest backgroundFetchPar" (go BackgroundPar)+ ]
tests/TestExampleDataSource.hs view
@@ -1,4 +1,13 @@-{-# LANGUAGE OverloadedStrings, RebindableSyntax #-}+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE CPP, OverloadedStrings, RebindableSyntax, MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-} module TestExampleDataSource (tests) where import Haxl.Prelude as Haxl@@ -7,14 +16,17 @@ import Haxl.Core.Monad (unsafeLiftIO) import Haxl.Core -import qualified Data.HashMap.Strict as HashMap import Test.HUnit import Data.IORef+import Data.Maybe import Control.Exception+import System.Environment+import System.FilePath import ExampleDataSource import LoadCache +testEnv :: IO (Env () ()) testEnv = do -- To use a data source, we need to initialize its state: exstate <- ExampleDataSource.initGlobalState@@ -23,7 +35,9 @@ let st = stateSet exstate stateEmpty -- Create the Env:- initEnv st ()+ env <- initEnv st ()+ return env{ flags = (flags env){+ report = setReportFlag ReportFetchStats defaultReportFlags } } tests = TestList [@@ -31,8 +45,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. @@ -47,12 +65,16 @@ assertEqual "runTests" x (2 + 3) -- Should be just one fetching round:- Stats s <- readIORef (statsRef env)- assertEqual "rounds" 1 (length s)+ Stats stats <- readIORef (statsRef env)+ putStrLn (ppStats (Stats stats))+ assertEqual "rounds" 1 (length stats) -- With two fetches:- let reqs = case head s of RoundStats m -> HashMap.lookup "ExampleDataSource" m- assertEqual "reqs" (Just 2) reqs+ assertBool "reqs" $+ case stats of+ [FetchStats{..}] ->+ fetchDataSource == "ExampleDataSource" && fetchBatchSize == 2+ _otherwise -> False -- Test side-effect ordering @@ -90,8 +112,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@@ -119,24 +141,84 @@ 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+ let env' = env { flags = (flags env){trace = 3} }++ let x = memo (CountAardvarks "ababa") $ do+ a <- length <$> listWombats 10+ b <- length <$> listWombats 20+ return (a + b)++ r <- runHaxl env' $ x + x + countAardvarks "baba"++ assertEqual "memoTest1" 62 r++ 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+ r <- runHaxl env $ Haxl.try $ countAardvarks "BANG"- assertBool "exception" $+ assertBool "exception1" $ case r of Left (ErrorCall "BANG") -> True _ -> False r <- runHaxl env $ Haxl.try $ countAardvarks "BANG2"- assertBool "exception" $+ assertBool "exception2" $ case r of Left (ErrorCall "BANG2") -> True _ -> False + -- In this test, BANG3 is an asynchronous exception (ThreadKilled),+ -- so we should see that instead of the exception on the left.+ -- Furthermore, it doesn't get caught by Haxl.try, and we have to+ -- catch it outside of runHaxl.+ env <- testEnv+ r <- Control.Exception.try $ runHaxl env $ Haxl.try $+ (length <$> listWombats 100) + countAardvarks "BANG3"+ print r+ assertBool "exception3" $+ case (r :: Either AsyncException (Either SomeException Int)) of+ Left ThreadKilled -> True+ _ -> False+ -- Test that we can load the cache from a dumped copy of it, and then dump it -- again to get the same result. dumpCacheTest = TestCase $ do env <- testEnv runHaxl env loadCache str <- runHaxl env dumpCacheAsHaskell- loadcache <- readFile "sigma/haxl/core/tests/LoadCache.txt"- assertEqual "dumpCacheAsHaskell" str loadcache+ lcPath <-loadCachePath+ loadcache <- readFile lcPath+ -- The order of 'cacheRequest ...' calls is nondeterministic and+ -- differs among GHC versions, so we sort the lines for comparison.+ assertEqual "dumpCacheAsHaskell" (sort $ lines loadcache) (sort $ lines str)+ where+ loadCachePath = do+ lcEnv <- lookupEnv "LOADCACHE"+ return $ fromMaybe (dropFileName __FILE__ </> "LoadCache.txt") lcEnv
+ tests/TestMain.hs view
@@ -0,0 +1,17 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE CPP, OverloadedStrings #-}+module Main where++import Test.Framework (defaultMain)+import Test.Framework.Providers.HUnit (hUnitTestToTests)+import AllTests++main :: IO ()+main = defaultMain $ hUnitTestToTests allTests
tests/TestTypes.hs view
@@ -1,42 +1,65 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}+ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE CPP #-} module TestTypes ( UserEnv , Haxl+ , HaxlEnv , lookupInput , Id(..) ) where import Data.Aeson-import Data.Text (Text)+import Data.Binary (Binary) import qualified Data.Text as Text-import qualified Data.HashMap.Strict as HashMap+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap as KeyMap+import Data.Aeson.Key (toText)+#else+import qualified Data.HashMap.Strict as KeyMap+#endif import Data.Hashable import Data.Typeable import Haxl.Core +#if !MIN_VERSION_aeson(2,0,0)+type Key = Text.Text++toText :: Key -> Text.Text+toText = id+#endif+ type UserEnv = Object-type Haxl a = GenHaxl UserEnv a+type Haxl a = GenHaxl UserEnv () a+type HaxlEnv = Env UserEnv () -lookupInput :: FromJSON a => Text -> Haxl a+lookupInput :: FromJSON a => Key -> Haxl a lookupInput field = do- mb_val <- env (HashMap.lookup field . userEnv)+ mb_val <- env (KeyMap.lookup field . userEnv) case mb_val of Nothing ->- throw (NotFound (Text.concat ["field ", field, " was not found."]))+ throw (NotFound (Text.concat ["field ", toText field, " was not found."])) Just val -> case fromJSON val of Error str -> throw (UnexpectedType (Text.concat- ["field ", field, ": ", Text.pack str]))+ ["field ", toText field, ": ", Text.pack str])) Success a -> return a newtype Id = Id Int- deriving (Eq, Ord, Enum, Num, Integral, Real, Hashable, Typeable,+ deriving (Eq, Ord, Binary, Enum, Num, Integral, Real, Hashable, Typeable, ToJSON, FromJSON) instance Show Id where
+ tests/TestUtils.hs view
@@ -0,0 +1,82 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+module TestUtils+ ( makeTestEnv+ , expectResultWithEnv+ , expectResult+ , expectFetches+ , testinput+ , id1, id2, id3, id4+ ) where++import TestTypes+import MockTAO+import Haxl.DataSource.ConcurrentIO++import Data.IORef+import Data.Aeson+import Test.HUnit+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap as KeyMap+#else+import qualified Data.HashMap.Strict as KeyMap+#endif++import Haxl.Core++import Prelude()+import Haxl.Prelude++testinput :: Object+testinput = KeyMap.fromList [+ "A" .= (1 :: Int),+ "B" .= (2 :: Int),+ "C" .= (3 :: Int),+ "D" .= (4 :: Int) ]++id1 :: Haxl Id+id1 = lookupInput "A"++id2 :: Haxl Id+id2 = lookupInput "B"++id3 :: Haxl Id+id3 = lookupInput "C"++id4 :: Haxl Id+id4 = lookupInput "D"++makeTestEnv :: Bool -> IO HaxlEnv+makeTestEnv future = do+ tao <- MockTAO.initGlobalState future+ stio <- mkConcurrentIOState+ let st = stateSet stio $ stateSet tao stateEmpty+ env <- initEnv st testinput+ return env { flags = (flags env) {+ report = setReportFlag ReportFetchStats defaultReportFlags } }++expectResultWithEnv+ :: (Eq a, Show a) => a -> Haxl a -> HaxlEnv -> Assertion+expectResultWithEnv result haxl env = do+ a <- runHaxl env haxl+ assertEqual "result" result a++expectResult :: (Eq a, Show a) => a -> Haxl a -> Bool -> Assertion+expectResult result haxl future = do+ env <- makeTestEnv future+ expectResultWithEnv result haxl env++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,87 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++module WorkDataSource (+ mkWorkState,+ work,+ ) where++import Haxl.Prelude+import Prelude ()++import Haxl.Core++import Control.Exception+import Data.Hashable+import Data.Typeable+import Control.Monad (void)+import Control.Concurrent.MVar+++data Work a where+ Work :: Integer -> Work Integer+ deriving Typeable++deriving instance Eq (Work a)+deriving instance Show (Work a)+instance ShowP Work where showp = show++instance Hashable (Work a) where+ hashWithSalt s (Work a) = hashWithSalt s (0::Int,a)++instance DataSourceName Work where+ dataSourceName _ = "Work"++instance StateKey Work where+ data State Work = WorkState++newtype Service = Service (MVar [IO ()])++run :: Work a -> IO a+run (Work n) = evaluate (sum [1..n]) >> return n++mkService :: IO Service+mkService = Service <$> newMVar []++process :: Service -> IO ()+process (Service q) = do+ r <- swapMVar q []+ sequence_ r++enqueue :: Service -> Work a -> IO (IO (Either SomeException a))+enqueue (Service q) w = do+ res <- newEmptyMVar+ let r = do+ v <- Control.Exception.try $ run w+ putMVar res v+ modifyMVar_ q (return . (:) r)+ return (takeMVar res)++instance DataSource u Work where+ fetch = backgroundFetchAcquireReleaseMVar+ mkService+ (\_ -> return ())+ -- pretend we are ready so that process does the work+ (\_ _ m -> void $ tryPutMVar m ())+ process+ enqueue+++mkWorkState :: State Work+mkWorkState = WorkState++work :: Integer -> GenHaxl u w Integer+work n = dataFetch (Work n)
+ tests/WriteTests.hs view
@@ -0,0 +1,300 @@+{-+ Copyright (c) Meta Platforms, Inc. and affiliates.+ All rights reserved.++ This source code is licensed under the BSD-style license found in the+ LICENSE file in the root directory of this source tree.+-}++{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+module WriteTests (tests) where++import Test.HUnit++import Control.Arrow+import Control.Concurrent+import Data.Either+import Data.Foldable+import Data.Hashable+import Data.IORef+import qualified Data.Text as Text++import Haxl.Core.Monad (mapWrites, mapWriteTree, flattenWT, WriteTree)+import Haxl.Core+import Haxl.Prelude as Haxl++-- A fake data source+data SimpleDataSource a where+ GetNumber :: SimpleDataSource Int++deriving instance Eq (SimpleDataSource a)+deriving instance Show (SimpleDataSource a)+instance ShowP SimpleDataSource where showp = show++instance Hashable (SimpleDataSource a) where+ hashWithSalt s GetNumber = hashWithSalt s (0 :: Int)++instance StateKey SimpleDataSource where+ data State SimpleDataSource = DSState++instance DataSourceName SimpleDataSource where+ dataSourceName _ = "SimpleDataSource"++instance DataSource u SimpleDataSource where+ fetch _st _flags _usr = SyncFetch $ Haxl.mapM_ fetch1+ where+ fetch1 :: BlockedFetch SimpleDataSource -> IO ()+ fetch1 (BlockedFetch GetNumber m) =+ threadDelay 1000 >> putSuccess m 37++newtype SimpleWrite = SimpleWrite Text+ deriving (Eq, Show, Ord)++assertEqualIgnoreOrder ::+ (Eq a, Show a, Ord a) => String -> [a] -> [a] -> Assertion+assertEqualIgnoreOrder msg lhs rhs =+ assertEqual msg (sort lhs) (sort rhs)++doInnerWrite :: GenHaxl u (WriteTree SimpleWrite) Int+doInnerWrite = do+ tellWrite $ SimpleWrite "inner"+ return 0++doOuterWrite :: GenHaxl u (WriteTree SimpleWrite) Int+doOuterWrite = do+ tellWrite $ SimpleWrite "outer1"++ doWriteMemo <- newMemoWith doInnerWrite+ let doMemoizedWrite = runMemo doWriteMemo+ _ <- doMemoizedWrite+ _ <- doMemoizedWrite++ tellWrite $ SimpleWrite "outer2"++ return 1++doNonMemoWrites :: GenHaxl u (WriteTree SimpleWrite) Int+doNonMemoWrites = do+ tellWrite $ SimpleWrite "inner"+ tellWriteNoMemo $ SimpleWrite "inner not memo"+ return 0++runHaxlWithWriteList :: Env u (WriteTree w) -> GenHaxl u (WriteTree w) a -> IO (a, [w])+runHaxlWithWriteList env haxl = second flattenWT <$> runHaxlWithWrites env haxl++writeSoundness :: Test+writeSoundness = TestCase $ do+ let numReps = 4++ -- do writes without memoization+ env1 <- emptyEnv ()+ (allRes, allWrites) <- runHaxlWithWriteList env1 $+ Haxl.sequence (replicate numReps doInnerWrite)++ assertBool "Write Soundness 1" $+ allWrites == replicate numReps (SimpleWrite "inner")+ assertBool "Write Soundness 2" $ allRes == replicate numReps 0++ -- do writes with memoization+ env2 <- emptyEnv ()++ (memoRes, memoWrites) <- runHaxlWithWriteList env2 $ do+ doWriteMemo <- newMemoWith doInnerWrite+ let memoizedWrite = runMemo doWriteMemo++ Haxl.sequence (replicate numReps memoizedWrite)++ assertBool "Write Soundness 3" $+ memoWrites == replicate numReps (SimpleWrite "inner")+ assertBool "Write Soundness 4" $ memoRes == replicate numReps 0++ -- do writes with interleaved memo+ env3 <- emptyEnv ()++ (ilRes, ilWrites) <- runHaxlWithWriteList env3 $ do+ doWriteMemo <- newMemoWith doInnerWrite+ let memoizedWrite = runMemo doWriteMemo++ Haxl.sequence $ replicate numReps (doInnerWrite *> memoizedWrite)++ assertBool "Write Soundness 5" $+ ilWrites == replicate (2*numReps) (SimpleWrite "inner")+ assertBool "Write Soundness 6" $ ilRes == replicate numReps 0++ -- do writes with nested memo+ env4 <- emptyEnv ()++ (nestRes, nestWrites) <- runHaxlWithWriteList env4 $ do+ doWriteMemo' <- newMemoWith doOuterWrite+ let memoizedWrite' = runMemo doWriteMemo'++ Haxl.sequence (replicate numReps memoizedWrite')++ let expWrites =+ [ SimpleWrite "outer1"+ , SimpleWrite "inner"+ , SimpleWrite "inner"+ , SimpleWrite "outer2"+ ]+ assertBool "Write Soundness 7" $+ nestWrites == fold (replicate numReps expWrites)+ assertBool "Write Soundness 8" $ nestRes == replicate numReps 1++ -- do both kinds of writes without memoization+ env5 <- emptyEnv ()+ (allRes, allWrites) <- runHaxlWithWriteList env5 $+ Haxl.sequence (replicate numReps doNonMemoWrites)++ assertBool "Write Soundness 9" $+ allWrites == replicate numReps (SimpleWrite "inner") +++ replicate numReps (SimpleWrite "inner not memo")+ assertBool "Write Soundness 10" $ allRes == replicate numReps 0++ -- do both kinds of writes with memoization+ env6 <- emptyEnv ()++ (memoRes, memoWrites) <- runHaxlWithWriteList env6 $ do+ doWriteMemo <- newMemoWith doNonMemoWrites+ let memoizedWrite = runMemo doWriteMemo++ Haxl.sequence (replicate numReps memoizedWrite)++ -- "inner not memo" only appears once in this test+ assertBool "Write Soundness 11" $+ memoWrites == replicate numReps (SimpleWrite "inner") +++ [SimpleWrite "inner not memo"]+ assertBool "Write Soundness 12" $ memoRes == replicate numReps 0++writeLogsCorrectnessTest :: Test+writeLogsCorrectnessTest = TestLabel "writeLogs_correctness" $ TestCase $ do+ e <- emptyEnv ()+ (_ , wrts) <- runHaxlWithWriteList e doNonMemoWrites+ assertEqualIgnoreOrder "Expected writes" [SimpleWrite "inner",+ SimpleWrite "inner not memo"] wrts+ wrtsNoMemo <- readIORef $ writeLogsRefNoMemo e+ wrtsMemo <- readIORef $ writeLogsRef e+ assertEqualIgnoreOrder "WriteTree not empty" [] $ flattenWT wrtsNoMemo+ assertEqualIgnoreOrder "WriteTree not empty" [] $ flattenWT wrtsMemo++mapWritesTest :: Test+mapWritesTest = TestLabel "mapWrites" $ TestCase $ do+ let funcSingle (SimpleWrite s) = SimpleWrite $ Text.toUpper s+ func = mapWriteTree funcSingle+ env0 <- emptyEnv ()+ (res0, wrts0) <- runHaxlWithWriteList env0 $ mapWrites func doNonMemoWrites+ assertEqual "Expected computation result" 0 res0+ assertEqualIgnoreOrder "Writes correctly transformed" [SimpleWrite "INNER",+ SimpleWrite "INNER NOT MEMO"] wrts0++ -- Writes should behave the same inside and outside mapWrites+ env1 <- emptyEnv ()+ (res1, wrts1) <- runHaxlWithWriteList env1 $ do+ outer <- doOuterWrite+ outerMapped <- mapWrites func doOuterWrite+ return $ outer == outerMapped+ assertBool "Results are identical" res1+ assertEqualIgnoreOrder+ "Writes correctly transformed, non-transformed writes preserved"+ [ SimpleWrite "outer1", SimpleWrite "inner"+ , SimpleWrite "inner", SimpleWrite "outer2"+ , SimpleWrite "OUTER1", SimpleWrite "INNER"+ , SimpleWrite "INNER", SimpleWrite "OUTER2"+ ]+ wrts1++ -- Memoization behaviour should be unaffected+ env2 <- emptyEnv ()+ (_res2, wrts2) <- runHaxlWithWriteList env2 $ do+ writeMemo <- newMemoWith doNonMemoWrites+ let doWriteMemo = runMemo writeMemo+ _ <- mapWrites func doWriteMemo+ _ <- doWriteMemo+ return ()+ -- "inner not memo" should appear only once+ assertEqualIgnoreOrder+ "Write correctly transformed under memoization"+ [ SimpleWrite "INNER"+ , SimpleWrite "inner"+ , SimpleWrite "INNER NOT MEMO"+ ]+ wrts2++ -- Same as previous, but the non-mapped computation is run first+ env3 <- emptyEnv ()+ (_res3, wrts3) <- runHaxlWithWriteList env3 $ do+ writeMemo <- newMemoWith doNonMemoWrites+ let doWriteMemo = runMemo writeMemo+ _ <- doWriteMemo+ _ <- mapWrites func doWriteMemo+ return ()+ -- "inner not memo" should appear only once+ assertEqualIgnoreOrder+ "Flipped: Write correctly transformed under memoization"+ [ SimpleWrite "inner"+ , SimpleWrite "INNER"+ , SimpleWrite "inner not memo"+ ]+ wrts3++ -- inner computation performs no writes+ env4 <- emptyEnv ()+ (res4, wrts4) <- runHaxlWithWriteList env4 $+ mapWrites func (return (0 :: Int))+ assertEqual "No Writes: Expected computation result" 0 res4+ assertEqualIgnoreOrder "No writes" [] wrts4++ -- inner computation throws an exception+ env5 <- emptyEnv ()+ (res5, wrts5) <- runHaxlWithWriteList env5 $ mapWrites func $ try $ do+ _ <- doNonMemoWrites+ _ <- throw (NotFound "exception")+ return 0+ assertBool "Throw: Expected Computation Result" $ isLeft+ (res5 :: Either HaxlException Int)+ assertEqualIgnoreOrder+ "Datasource writes correctly transformed"+ [ SimpleWrite "INNER"+ , SimpleWrite "INNER NOT MEMO"+ ]+ wrts5++ -- inner computation calls a datasource+ env6 <- initEnv (stateSet DSState stateEmpty) ()+ (res6, wrts6) <- runHaxlWithWriteList env6 $ mapWrites func $ do+ _ <- doNonMemoWrites+ dataFetch GetNumber++ assertEqual "Datasource: Expected Computation Result" 37 res6+ assertEqualIgnoreOrder+ "Datasource writes correctly transformed"+ [ SimpleWrite "INNER"+ , SimpleWrite "INNER NOT MEMO"+ ]+ wrts6++ -- inner computation calls a datasource, flipped calls+ env7 <- initEnv (stateSet DSState stateEmpty) ()+ (res7, wrts7) <- runHaxlWithWriteList env7 $ mapWrites func $ do+ df <- dataFetch GetNumber+ _ <- doNonMemoWrites+ return df++ assertEqual "Flipped Datasource: Expected Computation Result" 37 res7+ assertEqualIgnoreOrder+ "Flipped: Datasource writes correctly transformed"+ [ SimpleWrite "INNER"+ , SimpleWrite "INNER NOT MEMO"+ ]+ wrts7++tests = TestList+ [ TestLabel "Write Soundness" writeSoundness,+ TestLabel "writeLogs_correctness" writeLogsCorrectnessTest,+ TestLabel "mapWrites" mapWritesTest+ ]