pgmq-effectful-0.5.0.0: test/ClassificationSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module ClassificationSpec (tests) where
import Data.HashSet qualified as HashSet
import Data.Text (Text)
import Hasql.Errors qualified as HasqlErrors
import Pgmq.Effectful
( PgmqRuntimeError (..),
isTransient,
)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase)
-- | A server-reported statement error with the given SQLSTATE, wrapped the way
-- it actually arrives at 'isTransient': inside 'StatementSessionError'
-- (statement count, index, SQL, params, prepared flag) around
-- 'ServerStatementError' around 'ServerError', whose first field is the
-- five-character SQLSTATE.
serverStatementError :: Text -> PgmqRuntimeError
serverStatementError code =
PgmqSessionError
( HasqlErrors.StatementSessionError
1
0
"select 1"
[]
True
(HasqlErrors.ServerStatementError (HasqlErrors.ServerError code "boom" Nothing Nothing Nothing))
)
-- | SQLSTATEs that retries exist for: they arrive as server errors inside
-- 'StatementSessionError' and must classify transient.
transientStates :: [(Text, String)]
transientStates =
[ ("40001", "serialization_failure"),
("40P01", "deadlock_detected"),
("55P03", "lock_not_available"),
("57P01", "admin_shutdown"),
("57P02", "crash_shutdown"),
("57P03", "cannot_connect_now"),
("53100", "disk_full"),
("53200", "out_of_memory"),
("53300", "too_many_connections")
]
-- | Genuine statement bugs must stay permanent: retrying cannot fix them.
permanentStates :: [(Text, String)]
permanentStates =
[ ("23505", "unique_violation"),
("42P01", "undefined_table"),
("22P02", "invalid_text_representation")
]
tests :: TestTree
tests =
testGroup
"isTransient classification"
[ testCase "acquisition timeout is transient" $
assertBool "expected transient" (isTransient PgmqAcquisitionTimeout),
testCase "networking connection error is transient" $
assertBool
"expected transient"
( isTransient
(PgmqConnectionError (HasqlErrors.NetworkingConnectionError "refused"))
),
testCase "authentication error is not transient" $
assertBool
"expected permanent"
( not $
isTransient
(PgmqConnectionError (HasqlErrors.AuthenticationConnectionError "bad password"))
),
testCase "compatibility error is not transient" $
assertBool
"expected permanent"
( not $
isTransient
(PgmqConnectionError (HasqlErrors.CompatibilityConnectionError "version mismatch"))
),
testCase "other connection error is treated as transient" $
assertBool
"expected transient"
( isTransient
(PgmqConnectionError (HasqlErrors.OtherConnectionError "libpq says no"))
),
testCase "connection-drop session error is transient" $
assertBool
"expected transient"
( isTransient
(PgmqSessionError (HasqlErrors.ConnectionSessionError "dropped"))
),
testCase "driver session error is not transient" $
assertBool
"expected permanent"
( not $
isTransient
(PgmqSessionError (HasqlErrors.DriverSessionError "bug"))
),
testCase "missing-types session error is not transient" $
assertBool
"expected permanent"
( not $
isTransient
(PgmqSessionError (HasqlErrors.MissingTypesSessionError HashSet.empty))
),
testGroup
"server-reported SQLSTATEs (PGH-10)"
( [ testCase (show code <> " " <> name <> " is transient") $
assertBool "expected transient" (isTransient (serverStatementError code))
| (code, name) <- transientStates
]
<> [ testCase (show code <> " " <> name <> " is permanent") $
assertBool "expected permanent" (not (isTransient (serverStatementError code)))
| (code, name) <- permanentStates
]
),
testCase "row-count decode failure is not transient" $
assertBool
"expected permanent"
( not $
isTransient
( PgmqSessionError
( HasqlErrors.StatementSessionError
1
0
"select 1"
[]
True
(HasqlErrors.UnexpectedRowCountStatementError 1 1 0)
)
)
)
]