First draft of a tutorial (#15)

* add basic tutorial, re-export client stuff in HighLevel.Generated

* add a relative link in README.md

* forgot to document the language extensions

* cabal file fixup

* more context to compile-proto-file

* haskell syntax highlighting in markdown

* link to gRPC official tutorials for basic concepts

* add a note on how to build the examples

* prominent notice of required gRPC version

* fix typo

* do some error handling, show how to run the example executables

* use mapM
This commit is contained in:
Connor Clark 2017-04-30 15:38:29 -07:00 committed by GitHub
parent b550607f60
commit 169dbb7fff
9 changed files with 513 additions and 3 deletions

View file

@ -7,6 +7,8 @@ have extended and released under the same [`LICENSE`](./LICENSE)
Installation
------------
**The current version of this library requires gRPC version 1.2.0. Newer versions may work but have not been tested.**
Run the following command from the root of this repository to install the
`compile-proto-file` executable:
@ -17,6 +19,8 @@ $ nix-env -iA grpc-haskell -f release.nix
Usage
-----
There is a tutorial [here](examples/tutorial/TUTORIAL.md)
```bash
$ compile-proto-file --help
Dumps a compiled .proto file to stdout

View file

@ -0,0 +1,129 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
-- | Generated by Haskell protocol buffer compiler. DO NOT EDIT!
module Arithmetic where
import qualified Prelude as Hs
import qualified Proto3.Suite.DotProto as HsProtobuf
import qualified Proto3.Suite.Types as HsProtobuf
import qualified Proto3.Suite.Class as HsProtobuf
import qualified Proto3.Wire as HsProtobuf
import Control.Applicative ((<*>), (<|>))
import qualified Data.Text as Hs (Text)
import qualified Data.ByteString as Hs
import qualified Data.String as Hs (fromString)
import qualified Data.Vector as Hs (Vector)
import qualified Data.Int as Hs (Int16, Int32, Int64)
import qualified Data.Word as Hs (Word16, Word32, Word64)
import GHC.Generics as Hs
import GHC.Enum as Hs
import Network.GRPC.HighLevel.Generated as HsGRPC
import Network.GRPC.HighLevel.Client as HsGRPC
import Network.GRPC.HighLevel.Server as HsGRPC hiding (serverLoop)
import Network.GRPC.HighLevel.Server.Unregistered as HsGRPC
(serverLoop)
import Network.GRPC.LowLevel.Call as HsGRPC
data Arithmetic request response = Arithmetic{arithmeticAdd ::
request 'HsGRPC.Normal TwoInts OneInt ->
Hs.IO (response 'HsGRPC.Normal OneInt),
arithmeticRunningSum ::
request 'HsGRPC.ClientStreaming OneInt OneInt ->
Hs.IO (response 'HsGRPC.ClientStreaming OneInt)}
deriving Hs.Generic
arithmeticServer ::
Arithmetic HsGRPC.ServerRequest HsGRPC.ServerResponse ->
HsGRPC.ServiceOptions -> Hs.IO ()
arithmeticServer
Arithmetic{arithmeticAdd = arithmeticAdd,
arithmeticRunningSum = arithmeticRunningSum}
(ServiceOptions serverHost serverPort useCompression
userAgentPrefix userAgentSuffix initialMetadata sslConfig logger)
= (HsGRPC.serverLoop
HsGRPC.defaultOptions{HsGRPC.optNormalHandlers =
[(HsGRPC.UnaryHandler
(HsGRPC.MethodName "/arithmetic.Arithmetic/Add")
(HsGRPC.convertGeneratedServerHandler arithmeticAdd))],
HsGRPC.optClientStreamHandlers =
[(HsGRPC.ClientStreamHandler
(HsGRPC.MethodName "/arithmetic.Arithmetic/RunningSum")
(HsGRPC.convertGeneratedServerReaderHandler
arithmeticRunningSum))],
HsGRPC.optServerStreamHandlers = [],
HsGRPC.optBiDiStreamHandlers = [], optServerHost = serverHost,
optServerPort = serverPort, optUseCompression = useCompression,
optUserAgentPrefix = userAgentPrefix,
optUserAgentSuffix = userAgentSuffix,
optInitialMetadata = initialMetadata, optSSLConfig = sslConfig,
optLogger = logger})
arithmeticClient ::
HsGRPC.Client ->
Hs.IO (Arithmetic HsGRPC.ClientRequest HsGRPC.ClientResult)
arithmeticClient client
= (Hs.pure Arithmetic) <*>
((Hs.pure (HsGRPC.clientRequest client)) <*>
(HsGRPC.clientRegisterMethod client
(HsGRPC.MethodName "/arithmetic.Arithmetic/Add")))
<*>
((Hs.pure (HsGRPC.clientRequest client)) <*>
(HsGRPC.clientRegisterMethod client
(HsGRPC.MethodName "/arithmetic.Arithmetic/RunningSum")))
data TwoInts = TwoInts{twoIntsX :: Hs.Int32, twoIntsY :: Hs.Int32}
deriving (Hs.Show, Hs.Eq, Hs.Ord, Hs.Generic)
instance HsProtobuf.Named TwoInts where
nameOf _ = (Hs.fromString "TwoInts")
instance HsProtobuf.Message TwoInts where
encodeMessage _ TwoInts{twoIntsX = twoIntsX, twoIntsY = twoIntsY}
= (Hs.mconcat
[(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 1)
twoIntsX),
(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 2)
twoIntsY)])
decodeMessage _
= (Hs.pure TwoInts) <*>
(HsProtobuf.at HsProtobuf.decodeMessageField
(HsProtobuf.FieldNumber 1))
<*>
(HsProtobuf.at HsProtobuf.decodeMessageField
(HsProtobuf.FieldNumber 2))
dotProto _
= [(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 1)
(HsProtobuf.Prim HsProtobuf.Int32)
(HsProtobuf.Single "x")
[]
Hs.Nothing),
(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 2)
(HsProtobuf.Prim HsProtobuf.Int32)
(HsProtobuf.Single "y")
[]
Hs.Nothing)]
data OneInt = OneInt{oneIntResult :: Hs.Int32}
deriving (Hs.Show, Hs.Eq, Hs.Ord, Hs.Generic)
instance HsProtobuf.Named OneInt where
nameOf _ = (Hs.fromString "OneInt")
instance HsProtobuf.Message OneInt where
encodeMessage _ OneInt{oneIntResult = oneIntResult}
= (Hs.mconcat
[(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 1)
oneIntResult)])
decodeMessage _
= (Hs.pure OneInt) <*>
(HsProtobuf.at HsProtobuf.decodeMessageField
(HsProtobuf.FieldNumber 1))
dotProto _
= [(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 1)
(HsProtobuf.Prim HsProtobuf.Int32)
(HsProtobuf.Single "result")
[]
Hs.Nothing)]

View file

@ -0,0 +1,39 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE GADTs #-}
import Arithmetic
import Network.GRPC.HighLevel.Generated
clientConfig :: ClientConfig
clientConfig = ClientConfig { clientServerHost = "localhost"
, clientServerPort = 50051
, clientArgs = []
, clientSSLConfig = Nothing
}
main :: IO ()
main = withGRPCClient clientConfig $ \client -> do
(Arithmetic arithmeticAdd arithmeticRunningSum) <- arithmeticClient client
-- Request for the Add RPC
ClientNormalResponse (OneInt x) _meta1 _meta2 _status _details
<- arithmeticAdd (ClientNormalRequest (TwoInts 2 2) 1 [])
putStrLn ("2 + 2 = " ++ show x)
-- Request for the RunningSum RPC
ClientWriterResponse reply _streamMeta1 _streamMeta2 streamStatus streamDtls
<- arithmeticRunningSum $ ClientWriterRequest 1 [] $ \send -> do
eithers <- mapM send [OneInt 1, OneInt 2, OneInt 3]
:: IO [Either GRPCIOError ()]
case sequence eithers of
Left err -> error ("Error while streaming: " ++ show err)
Right _ -> return ()
case reply of
Just (OneInt y) -> print ("1 + 2 + 3 = " ++ show y)
Nothing -> putStrLn ("Client stream failed with status "
++ show streamStatus
++ " and details "
++ show streamDtls)
return ()

View file

@ -0,0 +1,50 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
import Arithmetic
import Network.GRPC.HighLevel.Generated
import Data.String (fromString)
handlers :: Arithmetic ServerRequest ServerResponse
handlers = Arithmetic { arithmeticAdd = addHandler
, arithmeticRunningSum = runningSumHandler
}
addHandler :: ServerRequest 'Normal TwoInts OneInt
-> IO (ServerResponse 'Normal OneInt)
addHandler (ServerNormalRequest _metadata (TwoInts x y)) = do
let answer = OneInt (x + y)
return (ServerNormalResponse answer
[("metadata_key_one", "metadata_value")]
StatusOk
"addition is easy!")
runningSumHandler :: ServerRequest 'ClientStreaming OneInt OneInt
-> IO (ServerResponse 'ClientStreaming OneInt)
runningSumHandler req@(ServerReaderRequest metadata recv) =
loop 0
where loop !i =
do msg <- recv
case msg of
Left err -> return (ServerReaderResponse
Nothing
[]
StatusUnknown
(fromString (show err)))
Right (Just (OneInt x)) -> loop (i + x)
Right Nothing -> return (ServerReaderResponse
(Just (OneInt i))
[]
StatusOk
"")
options :: ServiceOptions
options = defaultServiceOptions
main :: IO ()
main = arithmeticServer handlers options

View file

@ -0,0 +1,216 @@
## Introduction to gRPC-Haskell
*This tutorial assumes that you already have a basic understanding of gRPC as well as Haskell.* For an intoduction to the concepts of gRPC, see the [official tutorials](http://www.grpc.io/docs/tutorials/).
This will go through a basic example of using the library, with the `arithmetic` example in the `examples/arithmetic` directory. After cloning this repository, it would be a good idea to run `stack haddock` from within the repository directory to generate the documentation so you can read more about the functions and types we're using as we go. Also remember that [typed holes](https://wiki.haskell.org/GHC/Typed_holes) can be very handy.
To build the examples, you can run
```
$ stack build --flag grpc-haskell:with-examples
```
The gRPC service we will be implementing provides two amazing functions:
1. `Add`, which adds two integers.
2. `RunningSum`, which receives a stream of integers from the client and finally returns a single integer that is the sum of all the integers it has received.
You can run the examples by running `stack exec arithmetic-server` and `stack exec arithmetic-client`.
### Library Organization
**tl;dr: you probably only need to import `Network.GRPC.HighLevel.Generated`.** Other modules are exposed for advanced users only.
This library exposes quite a few modules, but you won't need to worry about most of them. They are currently organized based on the level of abstraction they afford over using the C [gRPC Core library](http://www.grpc.io/grpc/core/) directly:
* *`Unsafe`* modules directly wrap functions in the gRPC Core library. Using them directly is like using C: you need to think about memory management, pointers, and so on. The rest of the library is built on top of these functions and users of gRPC-haskell should never need to deal with the `Unsafe` modules directly.
* *`LowLevel`* modules still require an understanding of the gRPC Core library, but guarantee memory and thread safety. Only advanced users with special requirements would use `LowLevel` modules directly.
* *`HighLevel`* modules give you an opinionated Haskell interface to gRPC that should cover most use cases while (hopefully) being easy to use. You should only need to import the `Network.GRPC.HighLevel.Generated` module to start using the library. If you need to import other modules, we probably forgot to re-export something and you should open an issue or PR.
### Getting started
To start out, we need to generate code for our protocol buffers and RPCs. The `compile-proto-file` command is provided as part of `grpc-haskell`. You can either use `stack install` to install the command globally, or use `stack exec` within the `grpc-haskell` directory.
```
$ stack exec -- compile-proto-file --proto examples/echo/echo.proto > examples/echo/echo-hs/Echo.hs
```
The `.proto` file compiler always names the generated module the same as the `.proto` file, capitalizing the first letter if it is not already. Since our proto file is `arithmetic.proto`, the generated code should be placed in `Arithmetic.hs`.
The important things to notice in this generated file are:
1. For each proto message type, an equivalent Haskell type with the same name has been generated.
2. The `arithmeticServer` function takes a a record containing handlers for each RPC endpoint and some options, and starts a server. So, you just need to call this function to get a server running.
3, The `arithmeticClient` function takes a `Client` (which is just a proof that the gRPC core has been started) and gives you a record of functions that can be used to run RPCs.
### The server
First, we need to turn on some language extensions:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
```
All we need to do to run a server is call the `arithmeticServer` function:
```haskell
main :: IO ()
main = arithmeticServer handlers options
```
So we just need to define `handlers` and `options`.
`options` is easy-- it's just some basic options for the server. We can just use the default options for now, which will start the server listening on `localhost:50051`:
```haskell
options :: ServiceOptions
options = defaultServiceOptions
```
`handlers` is a bit more involved. Its type is `Arithmetic ServerRequest ServerResponse`. Values of this type contain a record field for each RPC defined in your `.proto` file.
```haskell
handlers :: Arithmetic ServerRequest ServerResponse
handlers = Arithmetic { arithmeticAdd = addHandler
, arithmeticRunningSum = runningSumHandler
}
```
You can think of the handlers as being of type `ServerRequest -> ServerResponse`, though there are a few more type parameters in there. The most important one is the first parameter, which specifies whether the RPC is streaming (`ClientStreaming`, `ServerStreaming`, or `BidiStreaming`) or not (`Normal`).
The `ServerRequest` passed to your handler contains all the tools you will need to handle the request, including:
1. The metadata the client sent with the request.
2. The protocol buffer message sent with the request, which has already been parsed into a Haskell type for you.
3. If it's a streaming request, you will also be given functions for sending or receiving messages in the stream.
#### The unary RPC handler for `Add`
So, let's pattern match on the `ServerRequest` for the `addHandler` function:
```haskell
addHandler (ServerNormalRequest metadata (TwoInts x y)) = -- to be continued!
```
The body of the `addHandler` function just needs to add `x` and `y` and then bundle the answer up in a `ServerResponse`:
```haskell
addHandler (ServerNormalRequest _metadata (TwoInts x y)) = do
let answer = OneInt (x + y)
return (ServerNormalResponse answer
[("metadata_key_one", "metadata_value")]
StatusOk
"addition is easy!")
```
Since this is a non-streaming "Normal" RPC, we use the the `ServerNormalResponse` constructor. Its parameters are the response message, some (optional) metadata key-value pairs, a status code, and a string with additional details about the status, which would normally be used to explain any errors in handling the request.
#### The client streaming handler for `RunningSum`
Now let's make our `runningSumHandler`. Since this is an RPC where the server reads from a stream of numbers, we pattern match on the `ServerReaderRequest` constructor:
```haskell
runningSumHandler req@(ServerReaderRequest metadata recv) = -- to be continued!
```
Unlike the unary "Normal" request handler, we don't get a message from the client in this pattern match. Instead, we get an IO action `recv`, which we can run to wait for the client to send us another message.
There are three possibilities when we try to receive another message from the client:
1. The RPC breaks with some gRPC error, such as losing the connection with the client.
2. We receive another message from the client.
3. The client has sent its last message and is waiting for a response.
We write a simple loop that keeps track of the running sum and finally sends off a `ServerReaderResponse` when the client finishes streaming or an error occurs:
```haskell
runningSumHandler req@(ServerReaderRequest metadata recv) =
loop 0
where loop !i =
do msg <- recv
case msg of
Left err -> return (ServerReaderResponse
Nothing
[]
StatusUnknown
(fromString (show err)))
Right (Just (OneInt x)) -> loop (i + x)
Right Nothing -> return (ServerReaderResponse
(Just (OneInt i))
[]
StatusOk
"")
```
The `ServerReaderResponse` type is almost the same as `ServerNormalResponse`, except that the first argument, the message to send back to the client, is optional. Otherwise, it takes metadata (which we leave empty), a status code, and a string containing more information about the status code.
### The client
The client-side code generated for us is `arithmeticClient`, which takes a `Client` as input and gives us a record containing actions that execute RPCs. To start up the C gRPC library and get a `Client`, we use `withGRPCClient`, which takes a `ClientConfig`:
```haskell
clientConfig :: ClientConfig
clientConfig = ClientConfig { clientServerHost = "localhost"
, clientServerPort = 50051
, clientArgs = []
, clientSSLConfig = Nothing
}
main :: IO ()
main = withGRPCClient clientConfig $ \client -> do
(Arithmetic arithmeticAdd arithmeticRunningSum) <- arithmeticClient client
-- to be continued!
```
Now that we are on the client side, the `Arithmetic` record contains functions that make RPC requests. You can think of these functions as roughly having the type `ClientRequest -> ClientResult`. Like before, the particular constructors will vary depending on whether the RPC is streaming or not.
#### Requesting unary RPC
Here we construct a `ClientNormalRequest`, which takes as input a message, a timeout in seconds, and metadata. The result is a `ClientNormalResponse`, containing the server's response, the initial and trailing metadata for the call, and the status and status details string.
```haskell
-- Request for the Add RPC
ClientNormalResponse (OneInt x) _meta1 _meta2 _status _details
<- arithmeticAdd (ClientNormalRequest (TwoInts 2 2) 1 [])
print ("2 + 2 = " ++ (show x))
```
#### Executing a client streaming RPC
Doing a streaming request is slightly trickier. As input to the streaming RPC action, we pass in another IO action that tells `grpc-haskell` what to send. It takes a `send` action as input. This is a bit convoluted, but it guarantees that you can't send streaming messages outside of the context of a streaming call!
```haskell
-- Request for the RunningSum RPC
ClientWriterResponse reply _streamMeta1 _streamMeta2 streamStatus streamDtls
<- arithmeticRunningSum $ ClientWriterRequest 1 [] $ \send -> do
eithers <- mapM send [OneInt 1, OneInt 2, OneInt 3]
:: IO [Either GRPCIOError ()]
case sequence eithers of
Left err -> error ("Error while streaming: " ++ show err)
Right _ -> return ()
```
Each `send` potentially returns an error message, with the type `Either GRPCIOError ()`. We use `sequence` to run all the `send` actions, and then use `sequence` again to collapse all the `Either`s. If an error is encountered while streaming, there's nothing we can do to salvage the RPC, so a more serious program would need to do some application-specific error-handling. Since this is just a tutorial, we print an error message and exit. Otherwise, we `return ()` to finish sending.
We can now inspect the `reply` to get our answer to the RPC.
```haskell
case reply of
Just (OneInt y) -> print ("1 + 2 + 3 = " ++ show y)
Nothing -> putStrLn ("Client stream failed with status "
++ show streamStatus
++ " and details "
++ show streamDtls)
```
To run the examples and see the requests, start the `arithmetic-server` process in the background, and then run the `arithmetic-client` process:
```
$ stack exec -- arithmetic-server &
$ stack exec -- arithmetic-client
```

View file

@ -0,0 +1,17 @@
syntax = "proto3";
package arithmetic;
service Arithmetic {
rpc Add (TwoInts) returns (OneInt) {}
rpc RunningSum (stream OneInt) returns (OneInt) {}
}
message TwoInts {
int32 x = 1;
int32 y = 2;
}
message OneInt {
int32 result = 1;
}

View file

@ -170,6 +170,51 @@ executable echo-server
hs-source-dirs: examples/echo/echo-hs
main-is: EchoServer.hs
executable arithmetic-server
if flag(with-examples)
build-depends:
base >=4.8 && <5.0
, async
, bytestring == 0.10.*
, containers ==0.5.*
, grpc-haskell
, optparse-generic
, proto3-suite
, proto3-wire
, text
, vector
other-modules:
Arithmetic
else
buildable: False
default-language: Haskell2010
ghc-options: -Wall -g -threaded -rtsopts -with-rtsopts=-N -O2
hs-source-dirs: examples/tutorial/
main-is: ArithmeticServer.hs
executable arithmetic-client
if flag(with-examples)
build-depends:
base >=4.8 && <5.0
, async
, bytestring == 0.10.*
, containers ==0.5.*
, grpc-haskell
, optparse-generic
, proto3-suite
, proto3-wire
, text
, vector
other-modules:
Arithmetic
else
buildable: False
default-language: Haskell2010
ghc-options: -Wall -g -threaded -rtsopts -with-rtsopts=-N -O2
hs-source-dirs: examples/tutorial/
main-is: ArithmeticClient.hs
executable echo-client
if flag(with-examples)
build-depends:

View file

@ -28,10 +28,17 @@ module Network.GRPC.HighLevel.Generated (
-- * Server Auth
, ServerSSLConfig(..)
-- * Client
, withGRPCClient
, ClientConfig(..)
, ClientRequest(..)
, ClientResult(..)
)
where
import Network.GRPC.HighLevel.Server
import Network.GRPC.HighLevel.Client
import Network.GRPC.LowLevel
import Network.GRPC.LowLevel.Call
import System.IO (hPutStrLn, stderr)
@ -84,3 +91,6 @@ defaultServiceOptions = ServiceOptions
, Network.GRPC.HighLevel.Generated.sslConfig = Nothing
, Network.GRPC.HighLevel.Generated.logger = hPutStrLn stderr
}
withGRPCClient :: ClientConfig -> (Client -> IO a) -> IO a
withGRPCClient c f = withGRPC $ \grpc -> withClient grpc c $ \client -> f client

View file

@ -54,8 +54,8 @@ data ClientSSLConfig = ClientSSLConfig
-- | Configuration necessary to set up a client.
data ClientConfig = ClientConfig {serverHost :: Host,
serverPort :: Port,
data ClientConfig = ClientConfig {clientServerHost :: Host,
clientServerPort :: Port,
clientArgs :: [C.Arg],
-- ^ Optional arguments for setting up the
-- channel on the client. Supplying an empty
@ -69,7 +69,7 @@ data ClientConfig = ClientConfig {serverHost :: Host,
}
clientEndpoint :: ClientConfig -> Endpoint
clientEndpoint ClientConfig{..} = endpoint serverHost serverPort
clientEndpoint ClientConfig{..} = endpoint clientServerHost clientServerPort
addMetadataCreds :: C.ChannelCredentials
-> Maybe C.ClientMetadataCreate