Add tests, fix some Valgrind errors (#25)

* add tests for server misbehavior

* fix leaks in pluck and next

* fix deadline leak

* fix slice leak

* always free metadata

* add full TODO in case we get rid of unregistered code

* shorter deadline to make test faster

* update for grpc 0.14.0

* fix another valgrind complaint
This commit is contained in:
Connor Clark 2016-06-13 07:25:32 -07:00 committed by Joel Stanley
parent 1f9953911c
commit 58661adf8b
8 changed files with 103 additions and 16 deletions

View file

@ -9,7 +9,7 @@
void grpc_haskell_free(char *debugMsg, void *ptr){ void grpc_haskell_free(char *debugMsg, void *ptr){
#ifdef GRPC_HASKELL_DEBUG #ifdef GRPC_HASKELL_DEBUG
printf("C wrapper: freeing %s, ptr: %p\n", debugMsg, ptr); printf("C wrapper: %s: freeing ptr: %p\n", debugMsg, ptr);
#endif #endif
free(ptr); free(ptr);
} }
@ -175,13 +175,14 @@ grpc_op* op_array_create(size_t n){
} }
void op_array_destroy(grpc_op* op_array, size_t n){ void op_array_destroy(grpc_op* op_array, size_t n){
#ifdef GRPC_HASKELL_DEBUG
printf("C wrapper: entered op_array_destroy\n");
#endif
for(int i = 0; i < n; i++){ for(int i = 0; i < n; i++){
grpc_op* op = op_array + i; grpc_op* op = op_array + i;
switch (op->op) { switch (op->op) {
case GRPC_OP_SEND_INITIAL_METADATA: case GRPC_OP_SEND_INITIAL_METADATA:
if(op->data.send_initial_metadata.count > 0){ metadata_free(op->data.send_initial_metadata.metadata);
metadata_free(op->data.send_initial_metadata.metadata);
}
break; break;
case GRPC_OP_SEND_MESSAGE: case GRPC_OP_SEND_MESSAGE:
grpc_byte_buffer_destroy(op->data.send_message); grpc_byte_buffer_destroy(op->data.send_message);
@ -224,6 +225,7 @@ void op_send_initial_metadata_empty(grpc_op *op_array, size_t i){
grpc_op *op = op_array + i; grpc_op *op = op_array + i;
op->op = GRPC_OP_SEND_INITIAL_METADATA; op->op = GRPC_OP_SEND_INITIAL_METADATA;
op->data.send_initial_metadata.count = 0; op->data.send_initial_metadata.count = 0;
op->data.send_initial_metadata.metadata = malloc(0*sizeof(grpc_metadata));
op->flags = 0; op->flags = 0;
op->reserved = NULL; op->reserved = NULL;
} }

View file

@ -37,6 +37,7 @@ import Control.Exception (bracket)
import Data.IORef (newIORef) import Data.IORef (newIORef)
import Data.List (intersperse) import Data.List (intersperse)
import Foreign.Marshal.Alloc (free, malloc) import Foreign.Marshal.Alloc (free, malloc)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek) import Foreign.Storable (peek)
import qualified Network.GRPC.Unsafe as C import qualified Network.GRPC.Unsafe as C
import qualified Network.GRPC.Unsafe.Constants as C import qualified Network.GRPC.Unsafe.Constants as C
@ -97,8 +98,8 @@ shutdownCompletionQueue (CompletionQueue{..}) = do
where drainLoop :: IO () where drainLoop :: IO ()
drainLoop = do drainLoop = do
deadline <- C.secondsToDeadline 1 ev <- C.withDeadlineSeconds 1 $ \deadline ->
ev <- C.grpcCompletionQueueNext unsafeCQ deadline C.reserved C.grpcCompletionQueueNext unsafeCQ deadline C.reserved
case (C.eventCompletionType ev) of case (C.eventCompletionType ev) of
C.QueueShutdown -> return () C.QueueShutdown -> return ()
C.QueueTimeout -> drainLoop C.QueueTimeout -> drainLoop
@ -170,7 +171,12 @@ serverRequestCall
let assembledCall = ServerCall rawCall metadataArrayPtr let assembledCall = ServerCall rawCall metadataArrayPtr
bbPtr Nothing deadline bbPtr Nothing deadline
return $ Right assembledCall return $ Right assembledCall
-- TODO: see TODO for failureCleanup in serverRequestCall. --TODO: the gRPC library appears to hold onto these pointers for a random
-- amount of time, even after returning from the only call that uses them.
-- This results in malloc errors if
-- gRPC tries to modify them after we free them. To work around it,
-- we sleep for a while before freeing the objects. We should find a
-- permanent solution that's more robust.
where failureCleanup deadline callPtr metadataArrayPtr bbPtr = forkIO $ do where failureCleanup deadline callPtr metadataArrayPtr bbPtr = forkIO $ do
threadDelaySecs 30 threadDelaySecs 30
grpcDebug "serverRequestCall(R): doing delayed cleanup." grpcDebug "serverRequestCall(R): doing delayed cleanup."

View file

@ -20,6 +20,7 @@ import qualified Network.GRPC.Unsafe as C (Call)
import qualified Network.GRPC.Unsafe.ByteBuffer as C import qualified Network.GRPC.Unsafe.ByteBuffer as C
import qualified Network.GRPC.Unsafe.Metadata as C import qualified Network.GRPC.Unsafe.Metadata as C
import qualified Network.GRPC.Unsafe.Op as C import qualified Network.GRPC.Unsafe.Op as C
import qualified Network.GRPC.Unsafe.Slice as C (Slice, freeSlice)
-- | Sum describing all possible send and receive operations that can be batched -- | Sum describing all possible send and receive operations that can be batched
-- and executed by gRPC. Usually these are processed in a handful of -- and executed by gRPC. Usually these are processed in a handful of
@ -39,7 +40,7 @@ data Op = OpSendInitialMetadata MetadataMap
-- 'withOpContexts'. -- 'withOpContexts'.
data OpContext = data OpContext =
OpSendInitialMetadataContext C.MetadataKeyValPtr Int OpSendInitialMetadataContext C.MetadataKeyValPtr Int
| OpSendMessageContext C.ByteBuffer | OpSendMessageContext (C.ByteBuffer, C.Slice)
| OpSendCloseFromClientContext | OpSendCloseFromClientContext
| OpSendStatusFromServerContext C.MetadataKeyValPtr Int C.StatusCode | OpSendStatusFromServerContext C.MetadataKeyValPtr Int C.StatusCode
B.ByteString B.ByteString
@ -90,7 +91,7 @@ createOpContext OpRecvCloseOnServer =
setOpArray :: C.OpArray -> Int -> OpContext -> IO () setOpArray :: C.OpArray -> Int -> OpContext -> IO ()
setOpArray arr i (OpSendInitialMetadataContext kvs l) = setOpArray arr i (OpSendInitialMetadataContext kvs l) =
C.opSendInitialMetadata arr i kvs l C.opSendInitialMetadata arr i kvs l
setOpArray arr i (OpSendMessageContext bb) = setOpArray arr i (OpSendMessageContext (bb,_)) =
C.opSendMessage arr i bb C.opSendMessage arr i bb
setOpArray arr i OpSendCloseFromClientContext = setOpArray arr i OpSendCloseFromClientContext =
C.opSendCloseClient arr i C.opSendCloseClient arr i
@ -109,7 +110,8 @@ setOpArray arr i (OpRecvCloseOnServerContext pcancelled) = do
-- | Cleans up an 'OpContext'. -- | Cleans up an 'OpContext'.
freeOpContext :: OpContext -> IO () freeOpContext :: OpContext -> IO ()
freeOpContext (OpSendInitialMetadataContext m _) = C.metadataFree m freeOpContext (OpSendInitialMetadataContext m _) = C.metadataFree m
freeOpContext (OpSendMessageContext bb) = C.grpcByteBufferDestroy bb freeOpContext (OpSendMessageContext (bb, s)) =
C.grpcByteBufferDestroy bb >> C.freeSlice s
freeOpContext OpSendCloseFromClientContext = return () freeOpContext OpSendCloseFromClientContext = return ()
freeOpContext (OpSendStatusFromServerContext metadata _ _ _) = freeOpContext (OpSendStatusFromServerContext metadata _ _ _) =
C.metadataFree metadata C.metadataFree metadata

View file

@ -5,6 +5,7 @@ module Network.GRPC.Unsafe where
import Control.Monad import Control.Monad
import Foreign.C.Types import Foreign.C.Types
import Foreign.Marshal.Alloc (free)
import Foreign.Ptr import Foreign.Ptr
import Foreign.Storable import Foreign.Storable
@ -115,8 +116,15 @@ instance Storable Event where
{#set grpc_event.success#} p $ if s then 1 else 0 {#set grpc_event.success#} p $ if s then 1 else 0
{#set grpc_event.tag#} p (unTag t) {#set grpc_event.tag#} p (unTag t)
-- | Used to unwrap structs from pointers. This is all part of a workaround
-- because the C FFI can't return raw structs directly. So we wrap the C
-- function, mallocing a temporary pointer. This function peeks the struct
-- within the pointer, then frees it.
castPeek :: Storable b => Ptr a -> IO b castPeek :: Storable b => Ptr a -> IO b
castPeek p = peek (castPtr p) castPeek p = do
val <- peek (castPtr p)
free p
return val
{#enum grpc_connectivity_state as ConnectivityState {underscoreToCase} {#enum grpc_connectivity_state as ConnectivityState {underscoreToCase}
deriving (Show, Eq)#} deriving (Show, Eq)#}

View file

@ -84,9 +84,15 @@ withByteStringAsByteBuffer bs f = do
bracket (byteStringToSlice bs) freeSlice $ \slice -> do bracket (byteStringToSlice bs) freeSlice $ \slice -> do
bracket (grpcRawByteBufferCreate slice 1) grpcByteBufferDestroy f bracket (grpcRawByteBufferCreate slice 1) grpcByteBufferDestroy f
-- TODO: Issue #5 -- Creates a 'ByteBuffer'. We also return the slice we needed to allocate to
createByteBuffer :: B.ByteString -> IO ByteBuffer -- create it. It is the caller's responsibility to free both when finished using
createByteBuffer bs = byteStringToSlice bs >>= flip grpcRawByteBufferCreate 1 -- the byte buffer. In most cases, one should prefer to use
-- 'withByteStringAsByteBuffer' if possible.
createByteBuffer :: B.ByteString -> IO (ByteBuffer, Slice)
createByteBuffer bs = do
slice <- byteStringToSlice bs
bb <- grpcRawByteBufferCreate slice 1
return (bb, slice)
copyByteBufferToByteString :: ByteBuffer -> IO B.ByteString copyByteBufferToByteString :: ByteBuffer -> IO B.ByteString
copyByteBufferToByteString bb = do copyByteBufferToByteString bb = do

View file

@ -1,3 +1,5 @@
{-# LANGUAGE StandaloneDeriving #-}
module Network.GRPC.Unsafe.Slice where module Network.GRPC.Unsafe.Slice where
#include <grpc/impl/codegen/slice.h> #include <grpc/impl/codegen/slice.h>
@ -12,6 +14,8 @@ import Foreign.Ptr
-- ByteStrings. This type is a pointer to a C type. -- ByteStrings. This type is a pointer to a C type.
{#pointer *gpr_slice as Slice newtype #} {#pointer *gpr_slice as Slice newtype #}
deriving instance Show Slice
-- TODO: we could also represent this type as 'Ptr Slice', by doing this: -- TODO: we could also represent this type as 'Ptr Slice', by doing this:
-- newtype Slice = Slice {#type gpr_slice#} -- newtype Slice = Slice {#type gpr_slice#}
-- This would have no practical effect, but it would communicate intent more -- This would have no practical effect, but it would communicate intent more

View file

@ -15,6 +15,7 @@ import qualified Network.GRPC.LowLevel.Client.Unregistered as U
import qualified Network.GRPC.LowLevel.Server.Unregistered as U import qualified Network.GRPC.LowLevel.Server.Unregistered as U
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit as HU (Assertion, import Test.Tasty.HUnit as HU (Assertion,
assertBool,
assertEqual, assertEqual,
assertFailure, assertFailure,
testCase, testCase,
@ -33,6 +34,8 @@ lowLevelTests = testGroup "Unit tests of low-level Haskell library"
-- , testWrongEndpoint -- , testWrongEndpoint
, testPayload , testPayload
, testPayloadUnregistered , testPayloadUnregistered
, testGoaway
, testSlowServer
] ]
testGRPCBracket :: TestTree testGRPCBracket :: TestTree
@ -146,12 +149,65 @@ testPayloadUnregistered =
return ("reply test", mempty, "details string") return ("reply test", mempty, "details string")
r @?= Right () r @?= Right ()
testGoaway :: TestTree
testGoaway =
csTest "Client handles server shutdown gracefully"
client
server
[("/foo", Normal)]
where
client c = do
rm <- clientRegisterMethod c "/foo" Normal
clientRequest c rm 10 "" mempty
clientRequest c rm 10 "" mempty
lastResult <- clientRequest c rm 1 "" mempty
assertBool "Client handles server shutdown gracefully" $
lastResult == unavailableStatus
||
lastResult == Left GRPCIOTimeout
server s = do
let rm = head (registeredMethods s)
serverHandleNormalCall s rm 11 mempty dummyHandler
serverHandleNormalCall s rm 11 mempty dummyHandler
return ()
testSlowServer :: TestTree
testSlowServer =
csTest "Client handles slow server response" client server [("/foo", Normal)]
where
client c = do
rm <- clientRegisterMethod c "/foo" Normal
result <- clientRequest c rm 1 "" mempty
assertBool "Client gets timeout or deadline exceeded" $
result == Left GRPCIOTimeout
||
result == deadlineExceededStatus
server s = do
let rm = head (registeredMethods s)
serverHandleNormalCall s rm 1 mempty $ \_ _ -> do
threadDelay (2*10^(6 :: Int))
return ("", mempty, mempty, StatusDetails "")
return ()
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Utilities and helpers -- Utilities and helpers
dummyMeta :: M.Map ByteString ByteString dummyMeta :: M.Map ByteString ByteString
dummyMeta = [("foo","bar")] dummyMeta = [("foo","bar")]
dummyHandler :: ByteString -> MetadataMap
-> IO (ByteString, MetadataMap, MetadataMap, StatusDetails)
dummyHandler _ _ = return ("", mempty, mempty, StatusDetails "")
unavailableStatus :: Either GRPCIOError a
unavailableStatus =
Left (GRPCIOBadStatusCode GrpcStatusUnavailable (StatusDetails ""))
deadlineExceededStatus :: Either GRPCIOError a
deadlineExceededStatus =
Left (GRPCIOBadStatusCode GrpcStatusDeadlineExceeded
(StatusDetails "Deadline Exceeded"))
nop :: Monad m => a -> m () nop :: Monad m => a -> m ()
nop = const (return ()) nop = const (return ())

View file

@ -142,8 +142,11 @@ payloadClient = do
destroyStatusCodePtr statusCodePtr destroyStatusCodePtr statusCodePtr
--verify response received --verify response received
responseRecv <- peek clientRecvBB responseRecv <- peek clientRecvBB
responseRecvBS <- copyByteBufferToByteString responseRecv let (ByteBuffer rawPtr) = responseRecv
responseRecvBS HU.@?= "hello you" if rawPtr == nullPtr
then error "Client got null pointer for received response!"
else do responseRecvBS <- copyByteBufferToByteString responseRecv
responseRecvBS HU.@?= "hello you"
grpcCompletionQueueShutdown cq grpcCompletionQueueShutdown cq
grpcCallDestroy clientCall grpcCallDestroy clientCall
--TODO: the grpc test drains the cq here --TODO: the grpc test drains the cq here