packages feed

hedis-0.16.1: test/PubSubTest.hs

{-# LANGUAGE CPP, OverloadedStrings #-}
{-# LANGUAGE BlockArguments #-}
module PubSubTest (testPubSubThreaded) where

import Control.Concurrent
import Control.Monad
import Control.Concurrent.Async
import Control.Exception
import Data.Function (fix)
import qualified Data.List
import Data.Text (Text)
import Data.Typeable
import Data.ByteString
import Control.Concurrent.STM
import System.Timeout (timeout)
import qualified Test.Framework as Test
import qualified Test.Framework.Providers.HUnit as Test (testCase)
import qualified Test.HUnit as HUnit

import Database.Redis

testPubSubThreaded :: [Connection -> Test.Test]
testPubSubThreaded =
  [ removeAllTest
  , callbackErrorTest
  , removeFromUnregister
  , pendingChannelsTrackingTest
  , subscribeReplyDecodingTracksPendingSets
  , withPubSubTest
  , withPubSubTimeoutTest
  , withPubSubTestBoth
  ]

-- | A handler label to be able to distinguish the handlers from one another
-- to help make sure we unregister the correct handler.
type HandlerLabel = Text

data TestMsg = MsgFromChannel HandlerLabel ByteString
             | MsgFromPChannel HandlerLabel RedisChannel ByteString
  deriving (Show, Eq)

type MsgVar = TVar [TestMsg]

-- | A handler that just writes the message into the TVar
handler :: HandlerLabel -> MsgVar -> MessageCallback
handler label ref msg = atomically $
  modifyTVar ref $ \x -> x ++ [MsgFromChannel label msg]

-- | A pattern handler that just writes the message into the TVar
phandler :: HandlerLabel -> MsgVar -> PMessageCallback
phandler label ref chan msg = atomically $
  modifyTVar ref $ \x -> x ++ [MsgFromPChannel label chan msg]

-- | Wait for a given message to be received
waitForMessage :: MsgVar -> HandlerLabel -> ByteString -> IO ()
waitForMessage ref label msg = atomically $ do
  let expected = MsgFromChannel label msg
  lst <- readTVar ref
  unless (expected `Prelude.elem` lst) retry
  writeTVar ref $ Prelude.filter (/= expected) lst

-- | Wait for a given pattern message to be received
waitForPMessage :: MsgVar -> HandlerLabel -> RedisChannel -> ByteString -> IO ()
waitForPMessage ref label chan msg = atomically $ do
  let expected = MsgFromPChannel label chan msg
  lst <- readTVar ref
  unless (expected `Prelude.elem` lst) retry
  writeTVar ref $ Prelude.filter (/= expected) lst

expectRedisChannels :: Connection -> [RedisChannel] -> IO ()
expectRedisChannels conn expected = do
  actual <- runRedis conn $ sendRequest ["PUBSUB", "CHANNELS"]
  case actual of
    Left err -> HUnit.assertFailure $ "Error geting channels: " ++ show err
    Right s -> HUnit.assertEqual "redis channels" (Data.List.sort s) (Data.List.sort expected)

-- | Test basic messages, plus using removeChannels
removeAllTest :: Connection -> Test.Test
removeAllTest conn = Test.testCase "Multithreaded Pub/Sub - basic" $ do
  msgVar <- newTVarIO []
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [("foo1", handler "InitialFoo1" msgVar), ("foo2", handler "InitialFoo2" msgVar)]
                              [("bar1:*", phandler "InitialBar1" msgVar), ("bar2:*", phandler "InitialBar2" msgVar)]
  withAsync (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True)) $ \_ -> do
    -- wait for initial
    atomically $ readTVar initialComplete >>= \b -> if b then return () else retry
    expectRedisChannels conn ["foo1", "foo2"]

    runRedis conn $ publish "foo1" "Hello"
    waitForMessage msgVar "InitialFoo1" "Hello"

    runRedis conn $ publish "bar2:zzz" "World"
    waitForPMessage msgVar "InitialBar2" "bar2:zzz" "World"

    -- subscribe to foo1 and bar1 again
    addChannelsAndWait ctrl [("foo1", handler "NewFoo1" msgVar)] [("bar1:*", phandler "NewBar1" msgVar)]
    expectRedisChannels conn ["foo1", "foo2"]

    runRedis conn $ publish "foo1" "abcdef"
    waitForMessage msgVar "InitialFoo1" "abcdef"
    waitForMessage msgVar "NewFoo1" "abcdef"

    -- unsubscribe from foo1 and bar1
    removeChannelsAndWait ctrl ["foo1", "unusued"] ["bar1:*", "unused:*"]
    expectRedisChannels conn ["foo2"]

    -- foo2 and bar2 are still subscribed
    runRedis conn $ publish "foo2" "12345"
    waitForMessage msgVar "InitialFoo2" "12345"

    runRedis conn $ publish "bar2:aaa" "0987"
    waitForPMessage msgVar "InitialBar2" "bar2:aaa" "0987"

data TestError = TestError ByteString
  deriving (Eq, Show)
instance Exception TestError

-- | Test an error thrown from a message handler
callbackErrorTest :: Connection -> Test.Test
callbackErrorTest conn = Test.testCase "Multithreaded Pub/Sub - error in handler" $ do
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [("foo", throwIO . TestError)] []

  thread <- async (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True))
  atomically $ readTVar initialComplete >>= \b -> if b then return () else retry

  runRedis conn $ publish "foo" "Hello"

  ret <- waitCatch thread
  case ret of
    Left (SomeException e) | cast e == Just (TestError "Hello") -> return ()
    _ -> HUnit.assertFailure $ "Did not properly throw error from message thread " ++ show ret

-- | Test removing channels by using the return value of 'addHandlersAndWait'.
removeFromUnregister :: Connection -> Test.Test
removeFromUnregister conn = Test.testCase "Multithreaded Pub/Sub - unregister handlers" $ do
  msgVar <- newTVarIO []
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [] []
  withAsync (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True)) $ \_ -> do
    atomically $ readTVar initialComplete >>= \b -> if b then return () else retry

    -- register to some channels
    void $ addChannelsAndWait ctrl
        [("abc", handler "InitialAbc" msgVar), ("xyz", handler "InitialXyz" msgVar)]
        [("def:*", phandler "InitialDef" msgVar), ("uvw", phandler "InitialUvw" msgVar)]
    expectRedisChannels conn ["abc", "xyz"]

    runRedis conn $ publish "abc" "Hello"
    waitForMessage msgVar "InitialAbc" "Hello"

    -- register to some more channels
    unreg <- addChannelsAndWait ctrl
        [("abc", handler "SecondAbc"  msgVar), ("123", handler "Second123" msgVar)]
        [("def:*", phandler "SecondDef" msgVar), ("890:*", phandler "Second890" msgVar)]
    expectRedisChannels conn ["abc", "xyz", "123"]

    -- check messages on all channels
    runRedis conn $ publish "abc" "World"
    waitForMessage msgVar "InitialAbc" "World"
    waitForMessage msgVar "SecondAbc" "World"

    runRedis conn $ publish "123" "World2"
    waitForMessage msgVar "Second123" "World2"

    runRedis conn $ publish "def:bbbb" "World3"
    waitForPMessage msgVar "InitialDef" "def:bbbb" "World3"
    waitForPMessage msgVar "SecondDef" "def:bbbb" "World3"

    runRedis conn $ publish "890:tttt" "World4"
    waitForPMessage msgVar "Second890" "890:tttt" "World4"

    -- unregister
    unreg

    -- we have no way of waiting until unregister actually happened, so just delay and hope
    threadDelay $ 1000*1000 -- 1 second
    expectRedisChannels conn ["abc", "xyz"]

    -- now only initial should be around. In particular, abc should still be subscribed
    runRedis conn $ publish "abc" "World5"
    waitForMessage msgVar "InitialAbc" "World5"

    runRedis conn $ publish "def:cccc" "World6"
    waitForPMessage msgVar "InitialDef" "def:cccc" "World6"

waitUntilPendingEmpty :: PubSubController -> IO ()
waitUntilPendingEmpty ctrl = do
  ret <- timeout (5 * 1000 * 1000) loop
  case ret of
    Nothing -> HUnit.assertFailure "Timed out waiting for pending PubSub channels to be cleared"
    Just _ -> return ()
  where
    loop = do
      pendingCh <- pendingChannels ctrl
      pendingPCh <- pendingPatternChannels ctrl
      unless (Prelude.null pendingCh && Prelude.null pendingPCh) $ do
        threadDelay (10 * 1000)
        loop

assertDoesNotHappen :: String -> IO a -> IO ()
assertDoesNotHappen label action = do
  ret <- timeout (700 * 1000) action
  case ret of
    Nothing -> return ()
    Just _ -> HUnit.assertFailure $ "Unexpectedly observed: " ++ label

-- | Verify exported pending sets track add/remove operations before Redis acknowledges requests.
pendingChannelsTrackingTest :: Connection -> Test.Test
pendingChannelsTrackingTest _ = Test.testCase "Multithreaded Pub/Sub - pending channels tracking" $ do
  msgVar <- newTVarIO []
  ctrl <- newPubSubController [] []

  _ <- addChannels ctrl
      [("pending:chan", handler "PendingChan" msgVar)]
      [("pending:*", phandler "PendingPattern" msgVar)]

  pendingCh <- pendingChannels ctrl
  pendingPCh <- pendingPatternChannels ctrl
  HUnit.assertBool "channel should be marked pending" ("pending:chan" `Prelude.elem` pendingCh)
  HUnit.assertBool "pattern channel should be marked pending" ("pending:*" `Prelude.elem` pendingPCh)

  removeChannels ctrl ["pending:chan"] ["pending:*"]

  pendingCh2 <- pendingChannels ctrl
  pendingPCh2 <- pendingPatternChannels ctrl
  HUnit.assertBool "removed channel should no longer be pending" (not $ "pending:chan" `Prelude.elem` pendingCh2)
  HUnit.assertBool "removed pattern channel should no longer be pending" (not $ "pending:*" `Prelude.elem` pendingPCh2)

-- | Exercise subscribe/unsubscribe decoding paths and ensure pending sets are drained per channel type.
subscribeReplyDecodingTracksPendingSets :: Connection -> Test.Test
subscribeReplyDecodingTracksPendingSets conn = Test.testCase "Multithreaded Pub/Sub - decode subscribe/unsubscribe replies" $ do
  msgVar <- newTVarIO []
  initialComplete <- newTVarIO False
  ctrl <- newPubSubController [] []

  withAsync (pubSubForever conn ctrl (atomically $ writeTVar initialComplete True)) $ \_ -> do
    atomically $ readTVar initialComplete >>= \b -> if b then return () else retry

    _ <- addChannels ctrl
        [("decode:chan", handler "DecodeChan" msgVar)]
        [("decode:*", phandler "DecodePattern" msgVar)]

    waitUntilPendingEmpty ctrl

    runRedis conn $ publish "decode:chan" "msg-1"
    waitForMessage msgVar "DecodeChan" "msg-1"

    runRedis conn $ publish "decode:abc" "msg-2"
    waitForPMessage msgVar "DecodePattern" "decode:abc" "msg-2"

    removeChannelsAndWait ctrl ["decode:chan"] ["decode:*"]

    waitUntilPendingEmpty ctrl

    runRedis conn $ publish "decode:chan" "msg-3"
    assertDoesNotHappen "channel callback after unsubscribe" $ waitForMessage msgVar "DecodeChan" "msg-3"

    runRedis conn $ publish "decode:def" "msg-4"
    assertDoesNotHappen "pattern callback after unsubscribe" $ waitForPMessage msgVar "DecodePattern" "decode:def" "msg-4"

withPubSubTest :: Connection -> Test.Test
withPubSubTest conn = Test.testCase "Multithreaded Pub/Sub - withPubSub" $ do
  lock <- newEmptyMVar
  _ <- forkIO $ do
    () <- takeMVar lock
    _ <- runRedis conn $ publish "foo9" "bar"
    pure ()
  result <- withPubSub conn ["foo9"] [] $ \messageSTM -> do
    putMVar lock ()
    atomically messageSTM
  case result of
    Message "foo9" "bar" -> pure ()
    x -> HUnit.assertFailure $ "Received unexpected message: " ++ show x

withPubSubTestBoth :: Connection -> Test.Test
withPubSubTestBoth conn = Test.testCase "Multithreaded Pub/Sub - withPubSub (both chan and pchan)" $ do
  lock <- newEmptyMVar
  _ <- forkIO $ do
    () <- takeMVar lock
    _ <- runRedis conn $ publish "foo100" "bar"
    _ <- runRedis conn $ publish "foo200" "bar"
    pure ()
  result <- withPubSub conn ["foo100"] ["foo2*"] $ \fetch -> do
    putMVar lock ()
    x <- timeout 1000000 $ do
      flip fix (False, False) \next (seenFoo100, seenFoo200) -> do
        unless (seenFoo100 && seenFoo200) do
          msg <- atomically fetch
          case msg of
            Message "foo100" "bar" -> putStrLn "A" >> next (True, seenFoo200)
            PMessage "foo2*" "foo200" "bar" -> putStrLn "B" >>next (seenFoo100, True)
            x -> HUnit.assertFailure $ "Received unexpected message: " ++ show x
    putStrLn "C"
    return x
  case result of
    Nothing -> HUnit.assertFailure $ "Messages were not received"
    Just{} -> pure ()

withPubSubTimeoutTest :: Connection -> Test.Test
withPubSubTimeoutTest conn = Test.testCase "Multithreaded Pub/Sub - withPubSub with timeout" $ do
  result <- withPubSub conn ["foo100"] [] $ \messageSTM -> do
    timeout (100000) $ atomically messageSTM
  case result of
    Nothing -> pure ()
    Just x -> HUnit.assertFailure $ "Expected to timeout without receiving a message, but received: " ++ show x