mariadb-haskell-0.1.0.0: validation/hs/Validation/FaultTransport.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Validation.FaultTransport where
import Data.Word (Word8)
import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.Bits
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Network.Socket as N
import qualified Network.Socket.ByteString as NS
import System.Timeout
import Data.IORef
import qualified System.IO.Streams as S
import Database.MySQL.Base
import qualified Database.MySQL.Connection as C
-- | Deterministic physical packets plus observable writes/finalizer count. This
-- deliberately bypasses authentication while retaining the real framing and
-- ownership guards; live loopback tests below cover actual socket behavior.
scripted :: ConnectionOptions -> [Packet] -> IO (MySQLConn,IO [Packet],IO Int)
scripted options packets = do
input <- S.fromList packets
sent <- newIORef []
closed <- newIORef 0
conn <- C.newConnection options input (\p -> modifyIORef' sent (p:)) (modifyIORef' closed (+1))
pure (conn,reverse <$> readIORef sent,readIORef closed)
okPacket :: Word8 -> Packet
okPacket seqN = encodeToPacket seqN (OK 0 0 2 0)
textTranscript :: [Packet]
textTranscript =
[putToPacket 1 (putLenEncInt 1)
,encodeToPacket 2 (ColumnDef "" "" "" "n" "" 63 11 mySQLTypeLong 0 0)
,encodeToPacket 3 (EOF 0 2)
,putToPacket 4 (putLenEncBytes "7")
,encodeToPacket 5 (EOF 0 2)]
-- | Real loopback faults armed by command bytes rather than timing sleeps. A
-- dropped COMMIT reply is read from the backend before disconnecting the client,
-- allowing a separate connection to check durability without replaying COMMIT.
data ProxyMode = FragmentTraffic Int | DropCommitReply | DropTargetReply deriving Show
data ProxyStats = ProxyStats { proxyCommits :: Int, proxyPackets :: Int, proxyCommands :: Int } deriving Show
withFaultProxy :: ConnectInfo -> ProxyMode -> (ConnectInfo -> IO ProxyStats -> IO a) -> IO a
withFaultProxy ci mode action = N.withSocketsDo $ bracket listener N.close $ \server -> do
case mode of
FragmentTraffic size | size <= 0 -> fail "proxy fragment size must be positive"
_ -> pure ()
N.SockAddrInet port _ <- N.getSocketName server
stats <- newIORef (ProxyStats 0 0 0)
finished <- newEmptyMVar
let stop worker = do
killThread worker
joined <- timeout 1000000 (takeMVar finished)
when (joined==Nothing) (fail "proxy worker did not terminate")
bracket (forkFinally (serve server stats) (const (putMVar finished ()))) stop $ \_ ->
action ci {ciHost="127.0.0.1",ciPort=port} (readIORef stats)
where
listener = bracketOnError (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ \sock -> do
N.setSocketOption sock N.ReuseAddr 1
N.bind sock (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))
N.listen sock 1
pure sock
serve listenerSocket stats = bracket (fst <$> N.accept listenerSocket) N.close $ \client -> do
addresses <- N.getAddrInfo (Just N.defaultHints {N.addrSocketType=N.Stream}) (Just (ciHost ci)) (Just (show (ciPort ci)))
address <- case addresses of
firstAddress:_ -> pure firstAddress
[] -> fail "proxy backend address unavailable"
bracket (N.socket (N.addrFamily address) N.Stream N.defaultProtocol) N.close $ \backend -> do
N.connect backend (N.addrAddress address)
armed <- newIORef False
done <- newEmptyMVar
let send sock packet = case mode of
FragmentTraffic size -> chunks sock size packet
_ -> NS.sendAll sock packet
upstream = forever $ do
(header,body) <- receivePacket client
let queryBody=if not (B.null body) && B.head body==3 then BC.map toUpperASCII (B.tail body) else ""
isCommit=queryBody=="COMMIT"
target="MB_STAGE1_FAULT" `B.isInfixOf` queryBody
when (not (B.null body) && B.head body `elem` [3,14,22,23,25,26,250])
(updateStats stats (\x -> x {proxyCommands=proxyCommands x+1}))
when isCommit (updateStats stats (\x -> x {proxyCommits=proxyCommits x+1}))
case mode of
DropCommitReply -> when isCommit (writeIORef armed True)
DropTargetReply -> when target (writeIORef armed True)
_ -> pure ()
send backend (header<>body)
downstream = do
(header,body) <- receivePacket backend
dropReply <- readIORef armed
unless dropReply $ do
updateStats stats (\x -> x {proxyPackets=proxyPackets x+1})
send client (header<>body)
downstream
-- Publish both worker finalizers before allowing cancellation of
-- the parent. Otherwise cancellation between forks can leak one.
mask $ \restore -> do
first <- forkFinally (restore upstream) (const (void (tryPutMVar done ())))
second <- forkFinally (restore downstream) (const (void (tryPutMVar done ())))
`onException` killThread first
restore (takeMVar done) `finally` (killThread first >> killThread second)
-- Upstream/downstream update different fields of the same record. Atomic
-- updates avoid a packet counter overwriting a concurrent command count.
updateStats ref change = atomicModifyIORef' ref (\stats -> (change stats, ()))
receivePacket sock = do
header <- exactly sock 4
let size=fromIntegral (B.index header 0) .|. (fromIntegral (B.index header 1) `shiftL` 8) .|. (fromIntegral (B.index header 2) `shiftL` 16)
body <- exactly sock size
pure (header,body)
exactly sock size = loop size []
where
loop 0 acc = pure (B.concat (reverse acc))
loop n acc = do
part <- NS.recv sock n
when (B.null part) (throwIO (userError "proxy peer closed"))
loop (n-B.length part) (part:acc)
chunks _ _ packet | B.null packet = pure ()
chunks sock size packet = NS.sendAll sock (B.take size packet) >> chunks sock size (B.drop size packet)
toUpperASCII ch | ch >= 'a' && ch <= 'z' = toEnum (fromEnum ch-32)
| otherwise = ch