stc-lang-1.0.0: stream-bench/algo.hs
{-# LANGUAGE ConstraintKinds, TypeApplications, OverloadedStrings,
PartialTypeSignatures, OverloadedLists, TypeFamilies, MultiWayIf
#-}
import Control.Concurrent (forkIO, killThread, newEmptyMVar, threadDelay)
import qualified Control.Concurrent as Conc
import qualified Control.Concurrent.BoundedChan as BC
import Control.Concurrent.MVar (putMVar, takeMVar)
import Control.DeepSeq (NFData, deepseq)
import Control.Exception (assert, bracket)
import Control.Monad
( (<=<)
, (>=>)
, forM
, forM_
, forever
, join
, replicateM
, unless
, void
, when
)
import Control.Monad.Generator (Generator, foldlGeneratorT, ioReaderToGenerator)
import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.List (ListT(ListT), runListT)
import Control.Monad.Reader (ReaderT, ask, lift, runReaderT)
import Control.Monad.SD
import Control.Monad.State.Class as S
( MonadState
, get
, gets
, modify
, put
, state
)
import Control.Monad.State.Lazy as S (StateT, runStateT)
import Data.Aeson ((.:), decode)
import qualified Data.Aeson as AE
import qualified Data.Aeson.Types as AE
import Data.Aeson.Types (parseMaybe)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Hashable
import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
import Data.Int (Int64)
import Data.List (nub)
import Data.Maybe (fromJust, fromMaybe, isNothing)
import Data.Monoid ((<>))
import Data.StateElement
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as Tx
import qualified Data.Text.Encoding as Tx
import qualified Data.Text.IO as Tx
import Data.Typeable (Typeable)
import qualified Data.UUID.Types as UUID
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
import qualified Data.Vector.Unboxed as UV
import qualified Data.Vector.Unboxed.Mutable as UMV
import Data.Word (Word32)
import qualified Data.Yaml as Yaml
import qualified Debug.Trace as Debug
import GHC.Exts (IsList, Item)
import GHC.Generics (Generic)
import Lens.Micro
import Lens.Micro.Aeson
import qualified MutableNFMap as NFMap
import qualified MutableSet as Set
import Prelude hiding (String, show)
import qualified Prelude as P
import qualified System.Clock as Clock
import System.Environment
import System.IO (Handle, IOMode(WriteMode), withFile)
import System.IO.Unsafe (unsafePerformIO)
import System.Random (randomIO)
import System.Random (randomIO, randomRIO)
import Text.Printf (hPrintf, printf)
import qualified Database.Redis as Redis
import qualified Kafka.Consumer as K
type String = BS.ByteString
type Message = LBS.ByteString
type Long = Int64
data Window = Window
{ seenCount :: IORef Long
, timestamp :: Text
} deriving (Eq, Generic)
instance Hashable Window where
hashWithSalt s w = hashWithSalt s (timestamp w)
instance NFData Window
instance AE.FromJSON String where
parseJSON = AE.withText "Expected String" $ pure . Tx.encodeUtf8
data Collector a = Collector
{ counts :: IORef (UMV.IOVector a)
, lastIndex :: IORef Int
}
collectorInitialSize :: Int
collectorInitialSize = 400
newCollector :: (MonadIO m, UMV.Unbox a) => m (Collector a)
newCollector =
liftIO $ Collector <$> (newIORef =<< UMV.unsafeNew collectorInitialSize) <*>
newIORef 0
push :: (MonadIO m, UMV.Unbox a) => Collector a -> a -> m ()
push Collector {..} i =
liftIO $ do
indx <- readIORef lastIndex
c <- readIORef counts
let l = UMV.length c
assert (indx <= l && indx > 0) (pure ())
c' <-
if l == indx
then do
new <- UMV.unsafeGrow c l
writeIORef counts new
pure new
else pure c
UMV.write c' indx i
modifyIORef' lastIndex succ
unsafeRead :: (MonadIO m, UMV.Unbox a) => Collector a -> m (UV.Vector a)
unsafeRead Collector {..} =
liftIO $ do
idx <- readIORef lastIndex
UV.unsafeFreeze . UMV.slice 0 idx =<< readIORef counts
data Statistics = Statistics
{ fetcherEventCount :: Collector Int
, pipelineEventCount :: Collector Word32
, redisWritesCount :: Collector Int
}
initStatistics :: IO Statistics
initStatistics = Statistics <$> newCollector <*> newCollector <*> newCollector
writeStatistics :: MonadIO m => Statistics -> Handle -> m ()
writeStatistics Statistics {..} h =
liftIO $ do
hPrintf
h
"%15s %15s %15s\n"
("Fetcher Events" :: P.String)
("Pipeline Events" :: P.String)
("Write events" :: P.String)
fec <- unsafeRead fetcherEventCount
pec <- unsafeRead pipelineEventCount
rwc <- unsafeRead redisWritesCount
UV.forM_ (UV.zip3 fec pec rwc) $ \(fcount, pcount, rcount) ->
hPrintf h "%-15d %-15d %-15d\n" fcount pcount rcount
let l1 = UV.length fec
l2 = UV.length pec
l3 = UV.length rwc
unless (l1 == l2 && l2 == l3) $
printf "Lengths were unequal: fec=%7d pec=%7d rwc=%7d\n" l1 l2 l3
fromRight :: Show a => Either a b -> b
fromRight = either (error . P.show) id
show :: Show a => a -> Text
show = Tx.pack . P.show
timeDivisor :: Long
timeDivisor = 10 * 1000
-- | Timeout for the timer trigger. In the actual benchmarks this is one second.
-- I choose 10 seconds here because in one second only very little happens
timeout :: Int
timeout = 1000 * 1000 * 10
eventGenerationStep = 100 * 1000
kafkaEventCount = 10 * 1000 * 1000
numCampaigns = 100
currentMilliSecs :: IO Long
currentMilliSecs =
(`div` (1000 * 1000)) . fromIntegral . Clock.toNanoSecs <$>
Clock.getTime Clock.Realtime
-- | Do this for `kafkaEventCount` many times
generateKafkaEvents :: V.Vector Text -> IO [AE.Value]
generateKafkaEvents ads = do
startTime <-
fromIntegral . (* eventGenerationStep) . Clock.toNanoSecs <$>
Clock.getTime Clock.Realtime
runListT $ do
n <- ListT $ pure [0 .. 10000 :: Word]
userId <- liftIO $ randomIO @UUID.UUID
pageId <- liftIO $ randomIO @UUID.UUID
ad <- randomIn ads
adType <- randomIn adTypes
eventType <- randomIn eventTypes
pure $
AE.object
[ "user_id" AE..= userId
, "page_id" AE..= pageId
, "ad_id" AE..= ad
, "ad_type" AE..= adType
, "event_type" AE..= eventType
, "event_time" AE..= (startTime + (n * 10) + skew + lateBy)
, "ip_address" AE..= ("1.2.3.4" :: Text)
]
where
adTypes =
V.fromList
["banner", "modal", "sponsored-search", "mail", "mobile" :: Text]
eventTypes = V.fromList ["view", "click", "purchase" :: Text]
skew = 0
lateBy = 0
randomIn l = (l V.!) <$> liftIO (randomRIO (0, V.length l - 1))
eventGenerationLoop :: ([Message] -> IO ()) -> IO ()
eventGenerationLoop writer = do
ads <-
V.fromList <$> replicateM (10 * numCampaigns) (UUID.toText <$> randomIO)
--ads <- Tx.lines <$> Tx.readFile "ad-ids.txt"
forM_ @[] @IO @Word [0,fromIntegral eventGenerationStep .. kafkaEventCount] $ \_ -> do
evs <- generateKafkaEvents ads
let evaluated = map AE.encode evs
evaluated `deepseq` pure ()
putStrLn "Writing Events"
writer evaluated
-- NOTE I guessed this number. I have not seen any timeout specification in the
-- benchmark. This is in milliseconds.
kafkaTimeout :: K.Timeout
kafkaTimeout = K.Timeout 4000
-- NOTE I also guessed this number. No idea whether this value is appropriate.
kafkaBatchSize :: K.BatchSize
kafkaBatchSize = K.BatchSize 400
readKafka :: K.KafkaConsumer -> Statistics -> IO [Message]
readKafka con stats = do
subsState <- fromRight <$> K.subscription con
--putStrLn $ "Subscription state: \n" <> P.show subsState
resps <- K.pollMessageBatch con kafkaTimeout (K.BatchSize batchSize)
oldCount <- readIORef msgsRead
time <- currentMilliSecs
oldTime <- readIORef timeFrame
let msgs =
map
(LBS.fromStrict . fromJust . K.crValue .
(\a -> assert (isNothing $ K.crKey a) a) .
fromRight)
resps
let !newCount = oldCount + length msgs
if (time - oldTime > 10000)
then do
writeIORef timeFrame time
writeIORef msgsRead 0
putStrLn $ "Fetched " <> P.show newCount <>
" messages in the last ~10 seconds"
push (fetcherEventCount stats) newCount
else writeIORef msgsRead newCount
-- I just put this assert in here for now so that we can reason
-- better about the structure of the kafka message
--msgs `deepseq` putStrLn "done"
pure msgs
where
{-# NOINLINE msgsRead #-}
{-# NOINLINE timeFrame #-}
msgsRead = unsafePerformIO $ newIORef 0
timeFrame = unsafePerformIO $ newIORef =<< currentMilliSecs
batchSize = 400
getIndex :: Int -> [a] -> Maybe a
getIndex n _
| n < 0 = Nothing
getIndex _ [] = Nothing
getIndex 0 (x:_) = Just x
getIndex n (_:xs) = getIndex (n - 1) xs
writeRedis :: _ -> _ -> Redis.Redis _
writeRedis campaign window = do
redisResponse <- getUUID (Tx.encodeUtf8 $ timestamp window)
windowUUID <-
case redisResponse of
Nothing -> do
windowUUID <- encodeUUID <$> liftIO randomIO
checkR_ $
Redis.hset
campaign
(Tx.encodeUtf8 $ timestamp window)
windowUUID
redisResponse2 <- getUUID "windows"
windowListUUID <-
case redisResponse2 of
Nothing -> do
rand <- encodeUUID <$> liftIO randomIO
checkR_ $ Redis.hset campaign "windows" rand
pure rand
Just uuid -> pure uuid
checkR_ $
Redis.lpush
windowListUUID
[Tx.encodeUtf8 $ timestamp window]
pure windowUUID
Just uuid -> pure uuid
checkR_ $ Redis.hincrby windowUUID "seen_count" . fromIntegral =<<
liftIO (readIORef (seenCount window))
liftIO $ writeIORef (seenCount window) 0
time <- BS.pack . P.show <$> liftIO currentMilliSecs
checkR_ $ Redis.hset windowUUID "time_updated" time
Redis.lpush "time_updated" [time]
-- NOTE This function is not necessary. It "only" forces the errors from the
-- redis database. You can drop it and all its uses if you want to describe
-- the algorithm. The reason I have it is so we notice if something goes
-- wrong with the redis database.
where
checkR_ :: (Monad f, Show err) => f (Either err a) -> f ()
checkR_ = (either (error . P.show) (const $ pure ()) =<<)
getUUID field =
either (const Nothing) (join . getIndex 0) <$>
Redis.hmget campaign [field]
encodeUUID = BS.pack . P.show :: UUID.UUID -> BS.ByteString
redisGet :: MonadIO m => _ -> BS.ByteString -> m (Maybe BS.ByteString)
redisGet redisConn =
liftIO . Redis.runRedis redisConn . fmap fromRight . Redis.get
isTheSame :: (Show a, Typeable a, Eq a, NFData a) => IO a -> STCLang a Bool
isTheSame init =
liftWithState init $ \new -> do
old <- get
let isOld = old == new
unless isOld $ put new
pure $ isOld
redisJoinStateInit :: IO (NFMap.Map _ _)
redisJoinStateInit = NFMap.new
newWindow :: MonadIO m => Long -> m Window
newWindow timeBucket =
liftIO $ Window <$> newIORef 0 <*> pure (show $ timeBucket * timeDivisor)
-- NOTE This function doesn't do much. I did make an extra function for this
-- because I am not sure why they changed this function to be so simple, and why
-- they removed the call to redis that I assume was in here. I have a suspicion,
-- that the simplification here means that this benchmark is not as it was
-- described in the paper and because of that I leave the function in so we
-- remember to check it later.
redisGetWindow :: MonadIO m => Long -> m (Maybe Window)
redisGetWindow timeBucket = Just <$> newWindow timeBucket
getWindow ::
(MonadIO m, MonadState (w, NFMap.Map Long (NFMap.Map String Window)) m)
=> Long
-> String
-> m Window
getWindow timeBucket campaignId = do
campaignWindows <- gets snd
bucketMapE <-
NFMap.lookup timeBucket campaignWindows >>= \case
Just m -> pure $ Left m
Nothing ->
redisGetWindow timeBucket >>= \case
Nothing -> do
m <- NFMap.new
NFMap.insert timeBucket m campaignWindows
pure $ Left m
Just redisWindow -> do
m <- NFMap.new
NFMap.insert timeBucket m campaignWindows
pure $ Right redisWindow
case bucketMapE of
Right w -> pure w
Left bucketMap ->
NFMap.lookup campaignId bucketMap >>= \case
Nothing -> do
window <-
maybe (newWindow timeBucket) pure =<<
redisGetWindow timeBucket
NFMap.insert campaignId window bucketMap
pure window
Just window -> pure window
-- | These are necessary because the Kafka client is an older version (0.8.2.1)
-- and does not support the `ApiVersionRequest` that the C-client library we use
-- under the hood sends in the beginning.
extraKafkaProperties ::
(IsList l, Item l ~ (s0, s1), IsString s1, IsString s0) => l
extraKafkaProperties =
[("api.version.request", "false"), ("broker.version.fallback", "0.8.2.1")]
type KafkaReader = IO LBS.ByteString
type CloseKafka = IO ()
type KafkaActions = (KafkaReader, CloseKafka)
cachedBackoffReader :: _ -> IO [a] -> IO (IO a)
cachedBackoffReader backoff refetch = do
cache <- newIORef []
let go =
readIORef cache >>= \case
[] -> do
new <- refetch
if null new
then putStrLn "Refetch returned empty response" >>
threadDelay backoff
else writeIORef cache new
go
(x:xs) -> writeIORef cache xs >> pure x
pure go
setupMockKafka :: _ -> IO KafkaActions
setupMockKafka _conf = do
kafkaVar <- newEmptyMVar
let kafkaWriter m = putMVar kafkaVar m
kafkaReader <- cachedBackoffReader 100 (takeMVar kafkaVar)
writerThread <- forkIO $ eventGenerationLoop kafkaWriter
pure (kafkaReader, killThread writerThread)
setupKafka :: Statistics -> AE.Value -> IO KafkaActions
setupKafka stats conf = do
print topic
print conf
print $ K.cpProps props
cons <- fromRight <$> (K.newConsumer props sub)
reader <- cachedBackoffReader 100 $ readKafka cons stats
pure (reader, maybe (pure ()) (error . P.show) =<< K.closeConsumer cons)
where
topic = K.TopicName $ conf ^?! key "kafka.topic" . _String
sub = K.topics [topic] <> K.offsetReset K.Latest
props
-- NOTE If I do not assign a group it fails immediately with
-- "unknown group".
=
K.groupId (K.ConsumerGroupId "ohua-stream-bench-group") <>
--K.extraProps extraKafkaProperties <>
--K.debugOptions [K.DebugAll] <>
K.brokersList
(map (\host ->
K.BrokerAddress $ host <> ":" <>
show (conf ^?! key "kafka.port" . _Integer))
(conf ^.. key "kafka.brokers" . values . _String))
-- | Reads the config file at the specified path and creates the connection
-- objects we need
setup :: FilePath -> IO (KafkaActions, Redis.Connection, Statistics)
setup loc = do
conf <- fromRight <$> Yaml.decodeFileEither @AE.Value loc
let rinfo =
Redis.defaultConnectInfo
{ Redis.connectHost =
conf ^?! key "redis.host" . _String . to Tx.unpack
}
-- cons <- fmap fromRight (K.newConsumer props sub)
stats <- initStatistics
(,,) <$> setupKafka stats conf <*> Redis.checkedConnect rinfo <*> pure stats
withInitial msg ac = do
putStrLn $ "Doing initial " <> msg <> "..."
r <- ac
putStrLn $ msg <> " done"
pure r
traceM :: Monad m => P.String -> m ()
traceM msg = Debug.trace msg $ pure ()
-- NOTE in the original implementation of the algorithm `rebalance` is the first
-- function called on the input stream. This distributes the messages
-- round-robin. This means we could also spawn multiple algorithm instances and
-- process multiple messages in parallel. But we'd have to ensure the timer
-- events are *not* round robin distributed!
algo kafkaReader redisConn stats
-- Allocate the basic functions --
= do
traceM "Start allocations"
let deserialize o =
either (error . ("Decoding error: " <>)) id $ do
result <- AE.eitherDecode o
flip AE.parseEither result $ \o ->
(,,,,,,) <$> (o .: "user_id" :: _ String) <*>
(o .: "page_id" :: _ String) <*>
(o .: "ad_id" :: _ String) <*>
(o .: "ad_type" :: _ String) <*>
(o .: "event_type" :: _ String) <*>
(read <$> o .: "event_time") <*>
(o .: "ip_address" :: _ String)
let evFilterFunc ~(_, _, _, _, t, _, _) = pure $ t == "view"
let project ~(_, _, i2, _, _, i5, _) = pure (i2, i5)
traceM "Allocating redis"
redisJoin <-
liftWithState redisJoinStateInit $ \(adId, v2) ->
fmap (, adId, v2) <$> do
st <- get
NFMap.lookup adId st >>= \case
Just cid -> pure $ Just cid
Nothing -> do
mcid <- redisGet redisConn adId
maybe
(liftIO $ putStrLn "Ad campaign not found in redis")
(\cid -> NFMap.insert adId cid st)
mcid
pure mcid
-- The campaign processor wrapped in the logic to separate handling of the
-- timing event, regular and filtered data.
emitCounter <-
liftWithState (pure 0 :: _ Word32) $ \case
Right _ -> modify succ
Left _ -> do
c <- get
put 0
liftIO $ putStrLn $ "Saw " <> P.show c <>
" events total in time window"
push (pipelineEventCount stats) c
traceM "Allocating campaign"
processCampaign <-
liftWithState ((,) <$> Set.new <*> NFMap.new) $ \case
Right (Just ev@(campaignId, _adId, eventTime)) -> do
flushCache <- gets fst
let timeBucket = eventTime `div` timeDivisor
window <- getWindow timeBucket campaignId
liftIO $ modifyIORef' (seenCount window) (+ 1)
let value = (campaignId, window)
Set.insert value flushCache
Left timerTrigger -> do
(s, cache) <- get
newCache <- Set.new
modify (\(_, o) -> (newCache, o))
asList <- Set.toList s
--liftIO $ printf "I touched %d campaigns" (length $ nub $ map fst asList)
let l = length asList
push (redisWritesCount stats) l
liftIO $ do
putStrLn $ "Initiated redis write of " <> P.show l <>
" events"
Redis.runRedis redisConn $
mapM_
(\(cid, window) -> do
c <- liftIO $ readIORef $ seenCount window
--liftIO $ printf "Writing count %4d for timestamp %v\n" c (timestamp window)
writeRedis cid window)
asList
_ -> pure ()
-- The condition we will use later for the if
evCheck <- isTheSame currentMilliSecs
-- Allocate signals
traceM "Allocating timer"
timerSig <-
liftSignal
(threadDelay timeout >> currentMilliSecs)
(withInitial "time" currentMilliSecs)
-- Not sure if this is a good idea but I initialize here by polling the
-- first message. Perhaps we should use a `Maybe` instead, however its not
-- particularly convenient yet in our model so I do this.
traceM "Allocating Kafka reader"
msgSig <- liftSignal kafkaReader (withInitial "read kafka" kafkaReader)
traceM "Allocating preprocessor"
filteredProcessor
-- NOTE keyBy partitions the operator state according to some key. If we
-- have time we should implement that too
<-
filterSignalM
evFilterFunc
(project >=> redisJoin
-- >=> keyBy 0
)
-- The actual algorithm
return $ \src -> do
timerEv <- timerSig src
msgEv <- msgSig src
-- Fork on whether this is a timing event
procInput <-
if_
(evCheck timerEv)
(Right <$> do
msg <- pure $ deserialize msgEv
join <$> filteredProcessor msg)
(pure $ Left timerEv)
emitCounter procInput
processCampaign procInput
-- data CollSt = CollSt
-- { states :: [S]
-- , signals :: [IO S]
-- }
--
-- instance Monoid CollSt where
-- mempty = CollSt [] []
-- CollSt st1 si1 `mappend` CollSt st2 si2 =
-- CollSt (st1 `mappend` st2) (si1 `mappend` si2)
algoSeq kafkaReader redisConn stats
-- Allocate the basic functions --
=
runSignals $ do
traceM "Start allocations"
let deserialize o =
either (error . ("Decoding error: " <>)) id $ do
result <- AE.eitherDecode o
flip AE.parseEither result $ \o ->
(,,,,,,) <$> (o .: "user_id" :: _ String) <*>
(o .: "page_id" :: _ String) <*>
(o .: "ad_id" :: _ String) <*>
(o .: "ad_type" :: _ String) <*>
(o .: "event_type" :: _ String) <*>
(read <$> o .: "event_time") <*>
(o .: "ip_address" :: _ String)
let evFilterFunc ~(_, _, _, _, t, _, _) = pure $ t == "view"
let project ~(_, _, i2, _, _, i5, _) = pure (i2, i5)
traceM "Allocating redis"
redisJoin <-
liftWithState redisJoinStateInit $ \(adId, v2) ->
fmap (, adId, v2) <$> do
st <- get
NFMap.lookup adId st >>= \case
Just cid -> pure $ Just cid
Nothing -> do
mcid <- redisGet redisConn adId
maybe
(liftIO $
putStrLn "Ad campaign not found in redis")
(\cid -> NFMap.insert adId cid st)
mcid
pure mcid
-- The campaign processor wrapped in the logic to separate handling of the
-- timing event, regular and filtered data.
emitCounter <-
liftWithState (pure 0 :: _ Word32) $ \case
Right _ -> modify succ
Left _ -> do
c <- get
put 0
liftIO $ putStrLn $ "Saw " <> P.show c <>
" events total in time window"
push (pipelineEventCount stats) c
traceM "Allocating campaign"
processCampaign <-
liftWithState ((,) <$> Set.new <*> NFMap.new) $ \case
Right (Just ev@(campaignId, _adId, eventTime)) -> do
flushCache <- gets fst
let timeBucket = eventTime `div` timeDivisor
window <- getWindow timeBucket campaignId
liftIO $ modifyIORef' (seenCount window) (+ 1)
let value = (campaignId, window)
Set.insert value flushCache
Left timerTrigger -> do
(s, cache) <- get
newCache <- Set.new
modify (\(_, o) -> (newCache, o))
asList <- Set.toList s
--liftIO $ printf "I touched %d campaigns" (length $ nub $ map fst asList)
let l = length asList
push (redisWritesCount stats) l
liftIO $ do
putStrLn $ "Initiated redis write of " <> P.show l <>
" events"
Redis.runRedis redisConn $
mapM_
(\(cid, window) -> do
c <- liftIO $ readIORef $ seenCount window
--liftIO $ printf "Writing count %4d for timestamp %v\n" c (timestamp window)
writeRedis cid window)
asList
_ -> pure ()
-- The condition we will use later for the if
evCheck <- isTheSame currentMilliSecs
-- Allocate signals
traceM "Allocating timer"
timerSig <-
liftSignal
(threadDelay timeout >> currentMilliSecs)
(withInitial "time" currentMilliSecs)
-- Not sure if this is a good idea but I initialize here by polling the
-- first message. Perhaps we should use a `Maybe` instead, however its not
-- particularly convenient yet in our model so I do this.
traceM "Allocating Kafka reader"
msgSig <- liftSignal kafkaReader (withInitial "read kafka" kafkaReader)
traceM "Allocating preprocessor"
filteredProcessor
-- NOTE keyBy partitions the operator state according to some key. If we
-- have time we should implement that too
<-
filterSignalM
evFilterFunc
(project >=> redisJoin
-- >=> keyBy 0
)
-- The actual algorithm
pure $ \src -> do
timerEv <- timerSig src
msgEv <- msgSig src
-- Fork on whether this is a timing event
procInput <-
if_
(evCheck timerEv)
(Right <$> do
msg <- pure $ deserialize msgEv
join <$> filteredProcessor msg)
(pure $ Left timerEv)
emitCounter procInput
processCampaign procInput
where
isTheSame init =
liftWithState init $ \new -> do
old <- get
let isOld = old == new
unless isOld $ put new
pure $ isOld
printSignalD = putStrLn
runOhuaM comp states = do
v <- V.thaw (V.fromList states)
a <- runReaderT comp v
(a, ) . V.toList <$> V.freeze v
if_ c t e = do
c' <- c
if c'
then t
else e
filterSignalM cond f =
pure $ \item -> if_ (cond item) (Just <$> f item) (pure Nothing)
liftWithState ::
(Typeable s, NFData s)
=> IO s
-> (SF s a b)
-> BuildSeqOhua (a -> SeqOhua b)
liftWithState state stateThread = do
s0 <- lift state
l <-
S.state $ \s ->
(length $ states s, s {states = states s ++ [toS s0]})
pure $ \a -> do
v <- ask
liftIO $ do
s <- fromS <$> MV.read v l
(b, s') <- runStateT (stateThread a) s
MV.write v l $ toS s'
pure b
liftSignal ::
(Typeable a, NFData a)
=> IO a
-> IO a
-> BuildSeqOhua (Signals -> SeqOhua a)
liftSignal s0 init = do
idx <-
S.state $ \s@CollSt {signals} ->
(length signals, s {signals = signals ++ [toS <$> s0]})
liftWithState init $ \(i, s) ->
if i == idx
then do
let my = fromS s
S.put my
pure my
else S.get
runSignals comp = do
(comp', s) <- S.runStateT comp mempty
chan <- BC.newBoundedChan 100
bracket
(do forM (zip [0 ..] $ signals s) $ \(idx, sig) ->
Conc.forkIO $ forever $ do
event <- sig
BC.writeChan chan $ Just (idx, event))
(\threads -> do
printSignalD "Killing signal threads"
mapM_ Conc.killThread threads)
(\_ -> do
printSignalD "signals done"
let signalGen =
ioReaderToGenerator @(Generator IO) (BC.readChan chan)
runOhuaM (smapGen comp' signalGen) $ states s)
smapGen f = foldlGeneratorT lift (\acc i -> (: acc) <$> f i) []
type BuildSeqOhua a = StateT CollSt IO a
type SeqOhua a = ReaderT (MV.IOVector S) IO a
main, main0 :: IO ()
main = main0
main0 = do
[confPath] <- getArgs
runSequential <- maybe False (/= "") <$> lookupEnv "SEQUENTIAL"
void $
bracket
(setup confPath)
(\((_, closeKafka), _redisConn, stats) -> do
putStrLn "Closing resources"
closeKafka
putStrLn "Writing Statistics"
withFile "ohua-statistics.txt" WriteMode $ \h ->
writeStatistics stats h
-- Redis.disconnect redisConn
)
(\((kafkaReader, _), redisConn, stats) ->
putStrLn "Starting execution" >>
if runSequential
then algoSeq kafkaReader redisConn stats
else runSignals (algo kafkaReader redisConn stats))