packages feed

rdkafka-hs-1.1.0.0: test/Kafka/Internal/CallbackOwnerSpec.hs

{-# LANGUAGE ForeignFunctionInterface #-}

module Kafka.Internal.CallbackOwnerSpec (tests) where

import Control.Concurrent (forkFinally, forkIOWithUnmask, threadDelay)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar, tryReadMVar)
import Control.Exception (MaskingState (MaskedInterruptible), SomeException, finally, getMaskingState, throwIO, try)
import Control.Monad (void)
import Data.Either (isLeft)
import Data.IORef (IORef, atomicModifyIORef', modifyIORef', newIORef, readIORef)
import Data.Map.Strict qualified as M
import Foreign.Ptr (FunPtr, castFunPtr, freeHaskellFunPtr)
import GHC.Conc (BlockReason (BlockedOnMVar), ThreadStatus (ThreadBlocked), threadStatus)
import Kafka.Internal.CallbackOwner
import System.Timeout (timeout)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

foreign import ccall "wrapper" wrap :: IO () -> IO (FunPtr (IO ()))

foreign import ccall "dynamic" invoke :: FunPtr (IO ()) -> IO ()

tests :: TestTree
tests =
  testGroup
    "Callback owner"
    [ testCase "bridge allocation executes masked before ownership is recorded" $ do
        freed <- newIORef M.empty
        owner <- newCallbackOwnerWith (countFree freed)
        observed <- newEmptyMVar
        done <- newEmptyMVar
        void $ forkIOWithUnmask $ \unmask -> do
          result <-
            try @SomeException $
              unmask $
                installCallback
                  owner
                  (getMaskingState >>= putMVar observed >> wrap (pure ()))
                  (const (pure ()))
          putMVar done result
        bounded (readMVar done) >>= either throwIO pure
        bounded (readMVar observed) >>= (@?= MaskedInterruptible)
        releaseCallbackOwner owner
        counts <- readIORef freed
        M.size counts @?= 1
        sum counts @?= 1,
      testCase "installed and replaced bridges release exactly once" $ do
        freed <- newIORef M.empty
        owner <- newCallbackOwnerWith (countFree freed)
        a <- wrap (pure ())
        b <- wrap (pure ())
        installCallback owner (pure a) (const (pure ()))
        installCallback owner (pure b) (const (pure ()))
        releaseUnattachedCallbacks owner
        releaseCallbackOwner owner
        readIORef freed >>= (@?= M.fromList [(castFunPtr a, 1), (castFunPtr b, 1)]),
      testCase "failed native install still owns its allocated bridge" $ do
        freed <- newIORef M.empty
        owner <- newCallbackOwnerWith (countFree freed)
        pointer <- wrap (pure ())
        result <- try @SomeException $ installCallback owner (pure pointer) (const (throwIO (userError "install")))
        assertBool (show result) (isLeft result)
        releaseUnattachedCallbacks owner
        readIORef freed >>= (@?= M.singleton (castFunPtr pointer) 1),
      testCase "failed allocation has no release" $ do
        freed <- newIORef M.empty
        owner <- newCallbackOwnerWith (countFree freed)
        result <- try @SomeException $ installCallback owner (throwIO (userError "allocate") :: IO (FunPtr (IO ()))) (const (pure ()))
        assertBool (show result) (isLeft result)
        releaseUnattachedCallbacks owner
        readIORef freed >>= (@?= M.empty),
      testCase "transfer preserves a callable bridge until client release" $ do
        freed <- newIORef M.empty
        called <- newIORef (0 :: Int)
        owner <- newCallbackOwnerWith (countFree freed)
        pointer <- wrap (modifyIORef' called (+ 1))
        installCallback owner (pure pointer) (const (pure ()))
        attachCallbackOwner owner
        releaseUnattachedCallbacks owner
        readIORef freed >>= (@?= M.empty)
        invoke pointer
        readIORef called >>= (@?= 1)
        releaseCallbackOwner owner
        releaseCallbackOwner owner
        readIORef freed >>= (@?= M.singleton (castFunPtr pointer) 1),
      testCase "transferred and released owners reject reuse before allocating" $ do
        owner <- newCallbackOwner
        allocated <- newIORef (0 :: Int)
        attachCallbackOwner owner
        let reject = do
              result <- try @SomeException $ installCallback owner (modifyIORef' allocated (+ 1) >> wrap (pure ())) (const (pure ()))
              assertBool (show result) (isLeft result)
              checked <- try @SomeException (checkCallbackOwner owner)
              assertBool (show checked) (isLeft checked)
              transferred <- try @SomeException (attachCallbackOwner owner)
              assertBool (show transferred) (isLeft transferred)
        reject
        releaseCallbackOwner owner
        reject
        readIORef allocated >>= (@?= 0),
      testCase "release retry does not repeat completed releases" $ do
        freed <- newIORef M.empty
        attempts <- newIORef (0 :: Int)
        owner <- newCallbackOwnerWith $ \pointer -> do
          attempt <- atomicModifyIORef' attempts (\n -> (n + 1, n + 1))
          if attempt == 2 then throwIO (userError "before free") else countFree freed pointer
        a <- wrap (pure ())
        b <- wrap (pure ())
        installCallback owner (pure a) (const (pure ()))
        installCallback owner (pure b) (const (pure ()))
        result <- try @SomeException (releaseCallbackOwner owner)
        assertBool (show result) (isLeft result)
        releaseCallbackOwner owner
        readIORef freed >>= (@?= M.fromList [(castFunPtr a, 1), (castFunPtr b, 1)]),
      testCase "concurrent releases join before returning" $ do
        entered <- newEmptyMVar
        resume <- newEmptyMVar
        freed <- newIORef M.empty
        owner <- newCallbackOwnerWith $ \pointer -> putMVar entered () >> bounded (readMVar resume) >> countFree freed pointer
        pointer <- wrap (pure ())
        installCallback owner (pure pointer) (const (pure ()))
        doneA <- newEmptyMVar
        doneB <- newEmptyMVar
        void $ forkFinally (releaseCallbackOwner owner) (putMVar doneA)
        bounded (readMVar entered)
        flip finally (putMVar resume ()) $ do
          second <- forkFinally (releaseCallbackOwner owner) (putMVar doneB)
          let awaitBlocked =
                threadStatus second >>= \status ->
                  if status == ThreadBlocked BlockedOnMVar then pure () else threadDelay 1000 >> awaitBlocked
          bounded awaitBlocked
          tryReadMVar doneA >>= maybe (pure ()) (const (assertFailure "first release returned before free"))
          tryReadMVar doneB >>= maybe (pure ()) (const (assertFailure "second release returned before free"))
        bounded (readMVar doneA) >>= either throwIO pure
        bounded (readMVar doneB) >>= either throwIO pure
        readIORef freed >>= (@?= M.singleton (castFunPtr pointer) 1)
    ]

countFree :: IORef (M.Map (FunPtr ()) Int) -> FunPtr () -> IO ()
countFree counts pointer = do
  modifyIORef' counts (M.insertWith (+) pointer 1)
  freeHaskellFunPtr pointer

bounded :: IO a -> IO a
bounded action = timeout 5000000 action >>= maybe (assertFailure "owner stage timed out") pure