packages feed

hasql-queue-1.0.1: test/Database/Hasql/Queue/IOSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ScopedTypeVariables   #-}
module Database.Hasql.Queue.IOSpec where
import           Control.Concurrent
import           Control.Concurrent.STM
import           Control.Concurrent.Async
import           Control.Exception as E
import           Control.Monad
import           Data.Aeson
import           Data.Function
import           Data.IORef
import           Data.List
import           Hasql.Queue.IO
import           Hasql.Queue.Migrate
import           Test.Hspec                     (SpecWith, Spec, describe, it, afterAll, beforeAll, runIO)
import           Test.Hspec.Expectations.Lifted
import           Data.List.Split
import           Database.Postgres.Temp as Temp
import           Data.Pool
import           Data.Foldable
import           Crypto.Hash.SHA1 (hash)
import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteString.Char8 as BSC
import           Data.ByteString (ByteString)
import           Hasql.Connection
import           Hasql.Session
import qualified Hasql.Encoders as E
import qualified Hasql.Decoders as D
import           Data.Int
import           Data.Typeable
import qualified Hasql.Queue.Internal as I
import           Hasql.Queue.Internal (Payload (..))

aroundAll :: forall a. ((a -> IO ()) -> IO ()) -> SpecWith a -> Spec
aroundAll withFunc specWith = do
  (var, stopper, asyncer) <- runIO $
    (,,) <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Nothing
  let theStart :: IO a
      theStart = do

        thread <- async $ do
          withFunc $ \x -> do
            putMVar var x
            takeMVar stopper
          pure $ error "Don't evaluate this"

        writeIORef asyncer $ Just thread

        either pure pure =<< (wait thread `race` takeMVar var)

      theStop :: a -> IO ()
      theStop _ = do
        putMVar stopper ()
        traverse_ cancel =<< readIORef asyncer

  beforeAll theStart $ afterAll theStop $ specWith

withConn :: Temp.DB -> (Connection -> IO a) -> IO a
withConn db f = do
  let connStr = toConnectionString db
  E.bracket (either (throwIO . userError . show) pure =<< acquire connStr) release f

runThrow :: Session a -> Connection -> IO a
runThrow sess conn = either (throwIO . I.QueryException) pure =<< run sess conn

getCount :: Connection -> IO Int64
getCount = runThrow I.getCount

getPayload :: Connection -> D.Value a -> I.PayloadId -> IO (Maybe (I.Payload a))
getPayload conn decoder payloadId = runThrow (I.getPayload decoder payloadId) conn

withSetup :: (Pool Connection -> IO ()) -> IO ()
withSetup f = either throwIO pure <=< withDbCache $ \dbCache -> do
  migratedConfig <- either throwIO pure =<<
      cacheAction
        (("~/.tmp-postgres/" <>) . BSC.unpack . Base64.encode . hash
          $ BSC.pack $ migrationQueryString "int4")
        (flip withConn $ flip migrate "int4")
        (verboseConfig <> cacheConfig dbCache)
  withConfig migratedConfig $ \db -> do
    f =<< createPool
      (either (throwIO . userError . show) pure =<< acquire (toConnectionString db))
      release
      2
      60
      50

channel :: ByteString
channel = "hey"

withConnection :: (Connection -> IO ()) -> Pool Connection -> IO ()
withConnection = flip withResource

runReadCommitted :: Pool Connection -> Session a -> IO a
runReadCommitted = flip withReadCommitted

withReadCommitted :: Session a -> Pool Connection -> IO a
withReadCommitted action pool = do
  let wrappedAction = do
        sql "BEGIN"
        r <- action
        sql "ROLLBACK"
        pure r
  withResource pool $ \conn ->
    either (throwIO . userError . show) pure =<< run wrappedAction conn

{-
runNoTransaction :: Pool Connection -> Session a -> IO a
runNoTransaction pool session = withResource pool $ \conn ->
  either (throwIO . userError . show) pure =<< run action conn
-}

data FailedwithDequeue = FailedwithDequeue
  deriving (Show, Eq, Typeable)

instance Exception FailedwithDequeue

spec :: Spec
spec = describe "Hasql.Queue.IO" $ do
  aroundAll withSetup $ describe "basic" $ do
    it "withDequeue blocks until something is enqueued: before" $ withConnection $ \conn -> do
      void $ enqueue channel conn E.int4 [1]
      res <- withDequeueWith @IOException mempty channel conn D.int4 1 1 pure
      res `shouldBe` [1]
      getCount conn `shouldReturn` 0

    it "withDequeue blocks until something is enqueued: during" $ withConnection $ \conn -> do
      afterActionMVar  <- newEmptyMVar
      beforeNotifyMVar <- newEmptyMVar

      let handlers = WithNotifyHandlers
            { withNotifyHandlersAfterAction = putMVar afterActionMVar ()
            , withNotifyHandlersBeforeNotification      = takeMVar beforeNotifyMVar
            }

      -- This is the definition of IO.dequeue
      resultThread <- async $ withDequeueWith @IOException handlers channel conn D.int4 1 1 pure
      takeMVar afterActionMVar

      void $ enqueue "hey" conn E.int4 [1]

      putMVar beforeNotifyMVar ()

      wait resultThread `shouldReturn` [1]

    it "withDequeue blocks until something is enqueued: after" $ withConnection $ \conn -> do
      resultThread <- async $ withDequeueWith @IOException mempty channel conn D.int4 1 1 pure
      void $ enqueue channel conn E.int4 [1]

      wait resultThread `shouldReturn` [1]

    it "withDequeue fails and sets the retries to +1" $ withConnection $ \conn -> do
      [payloadId] <- runThrow (I.enqueuePayload E.int4 [1]) conn
      handle (\FailedwithDequeue -> pure ()) $ withDequeue channel conn D.int4 0 1 $ \_ -> throwIO FailedwithDequeue
      Just Payload {..} <- getPayload conn D.int4 payloadId

      pState `shouldBe` I.Failed
      pAttempts  `shouldBe` 1

    it "withDequeue succeeds even if the first attempt fails" $ withConnection $ \conn -> do
      [payloadId] <- runThrow (I.enqueuePayload E.int4 [1]) conn

      ref <- newIORef (0 :: Int)

      withDequeueWith @FailedwithDequeue mempty channel conn D.int4 1 1 (\_ -> do
        count <- readIORef ref
        writeIORef ref $ count + 1
        when (count < 1) $ throwIO FailedwithDequeue
        pure '!') `shouldReturn` '!'

      Just Payload {..} <- getPayload conn D.int4 payloadId

      pState `shouldBe` I.Dequeued
      -- Failed attempts I guess
      pAttempts  `shouldBe` 1

    it "enqueues and dequeues concurrently withDequeue" $ \testDB -> do
      let withPool' = flip withConnection testDB
          elementCount = 1000 :: Int
          expected = [0 .. elementCount - 1]

      ref <- newTVarIO []

      loopThreads <- replicateM 35 $ async $ withPool' $ \c -> fix $ \next -> do
        lastCount <- withDequeue channel c D.int4 1 1 $ \[x] -> do
          atomically $ do
            xs <- readTVar ref
            writeTVar ref $ x : xs
            return $ length xs + 1

        when (lastCount < elementCount) next

      forM_ (chunksOf (elementCount `div` 11) expected) $ \xs -> forkIO $ void $ withPool' $ \c ->
         forM_ xs $ \i -> enqueue channel c E.int4 [fromIntegral i]

      _ <- waitAnyCancel loopThreads
      xs <- atomically $ readTVar ref
      let Just decoded = mapM (decode . encode) xs
      sort decoded `shouldBe` sort expected

    it "enqueue returns a PayloadId that cooresponds to the entry it added" $ withConnection $ \conn -> do
      [payloadId] <- runThrow (I.enqueuePayload E.int4 [1]) conn
      Just actual <- getPayload conn D.int4 payloadId

      pValue actual `shouldBe` 1