hasql-1.10: src/library-tests/Sharing/ByBug/ExceptionConnectionResetRaceSpec.hs
module Sharing.ByBug.ExceptionConnectionResetRaceSpec (spec) where
import Control.Concurrent
import Control.Exception
import Data.IORef
import Hasql.Connection qualified as Connection
import Hasql.Session qualified as Session
import Helpers.Dsls.Execution qualified as Execution
import Helpers.Scripts qualified as Scripts
import Helpers.Statements.SelectProvidedInt8 qualified as Statements
import System.Timeout
import Test.Hspec
import Prelude
spec :: SpecWith (Text, Word16)
spec = do
describe "Exception during session with concurrent access" do
it "Connection remains usable after exception in non-idle state with concurrent threads" \config -> Scripts.onPreparableConnection config \connection -> do
-- This test reproduces the bug fixed in commit 62ebef2.
-- The bug was that when an exception occurred during a session,
-- the connection state was put back into the MVar BEFORE resetting the connection.
-- This created a race condition where another thread could grab the corrupted connection.
-- We'll create a scenario where:
-- 1. Thread A starts a session that will throw an exception
-- 2. Thread B repeatedly tries to use the connection
-- 3. The exception in Thread A should not corrupt the connection for Thread B
-- Counter to track successful operations by Thread B
successCount <- newIORef (0 :: Int)
errorCount <- newIORef (0 :: Int)
-- Barrier to synchronize threads
startBarrier <- newEmptyMVar
doneBarrier <- newEmptyMVar
-- Thread A: Throws exceptions repeatedly
_ <- forkIO do
takeMVar startBarrier
replicateM_ 10 do
-- Use the connection and throw an exception during the session
_ <- try @SomeException do
Connection.use connection do
-- Start a transaction to put connection in non-idle state
Session.script "BEGIN"
-- Throw an exception while in transaction (non-idle state)
liftIO (throwIO (userError "Intentional exception"))
threadDelay 1000 -- Small delay to allow interleaving
putMVar doneBarrier ()
-- Thread B: Tries to use connection concurrently
_ <- forkIO do
takeMVar startBarrier
replicateM_ 20 do
result <- Connection.use connection (Execution.sessionByParams (Statements.SelectProvidedInt8 42))
case result of
Right 42 -> atomicModifyIORef' successCount (\n -> (n + 1, ()))
_ -> atomicModifyIORef' errorCount (\n -> (n + 1, ()))
threadDelay 500
putMVar doneBarrier ()
-- Start both threads
putMVar startBarrier ()
putMVar startBarrier ()
-- Wait for both threads to complete with a timeout
-- If the bug exists, threads may hang waiting for a corrupted connection
result <- timeout (5 * 1000000) do
-- 5 seconds timeout
takeMVar doneBarrier
takeMVar doneBarrier
case result of
Nothing -> do
-- Test timed out - this indicates the bug is present
expectationFailure "Test timed out waiting for threads to complete. This indicates the connection became deadlocked due to the race condition bug."
Just () -> do
-- Threads completed successfully
-- Check results
successes <- readIORef successCount
errors <- readIORef errorCount
-- Thread B should have succeeded at least some times
-- If the bug exists, we'd expect Thread B to get errors due to corrupted connection state
successes `shouldSatisfy` (> 0)
errors `shouldBe` 0
-- Verify connection is still usable after all this
finalResult <- Connection.use connection (Execution.sessionByParams (Statements.SelectProvidedInt8 99))
finalResult `shouldBe` Right 99