Before we can send RPC requests, we have to connect to a specific server
first. Once we have opened a connection to that server, we can send as many
RPC requests over that one connection as we wish. Connection abstracts over
this connection, and also maintains some information about the server.
We can make many RPC calls over the same connection.
NOTE: The default ReconnectPolicy is DontReconnect, as per the
spec (see ReconnectPolicy). You may wish to override this in order
to enable Wait for Ready semantics (retry connecting to a server
when it is not yet ready) as well as automatic reconnects (reconnecting
after a server disappears). The latter can be especially important
when there are proxies, which tend to drop connections after a certain
amount of time.
This is the client analogue of
serverVerifyHeaders; see detailed
discussion there.
Arguably, it is less essential to verify headers on the client: a
server must deal with all kinds of different clients, and might want to
know if any of those clients has expectations that it cannot fulfill. A
client however connects to a known server, and knows what information
it wants from the server.
Under normal circumstances, the grapesy client will only start using
compression once the server has informed it what compression algorithms
it supports. This means the first message will necessarily be
uncompressed. connCompression can be used to override this behaviour,
but should be used with care: if the server does not support the
selected compression algorithm, it will not be able to decompress any
messages sent by the client to the server.
See withRPC for making individual RPCs on the new
connection.
The connection to the server is set up asynchronously; the first call to
withRPC will block until the connection has been established.
If the server cannot be reached, the behaviour depends on
connReconnectPolicy: if the policy allows reconnection attempts, we will
wait the time specified by the policy and try again. This implements the gRPC
"Wait for ready" semantics.
If the connection to the server is lost after it has been established, any
currently ongoing RPC calls will be closed; attempts at further communication
on any of these calls will result in a ServerDisconnected exception being
thrown. If that exception is caught, and the ReconnectPolicy allows, we
will automatically try to re-establish a connection to the server. This can
be especially important when there is a proxy between the client and the
server, which may drop an existing connection after a certain period.
NOTE: The defaultReconnectPolicy is DontReconnect, as per the gRPC
specification of "Wait for ready" semantics. You may wish to override this
default.
Clients should prefer sending many calls on a single connection, rather than
sending few calls on many connections, as minimizing the number of
connections used via this interface results in better memory behavior. See
well-typed/grapesy#134
for discussion.
Warning:
Connections hold open resources and must be closed using closeConnection.
To prevent resource and memory leaks due to asynchronous exceptions, it is
recommended to use the bracketed function withConnection whenever
possible, and otherwise run functions that allocate and release a resource
with asynchronous exceptions masked, and ensure that every use allocate
operation is followed by the corresponding release operation even in the
presence of asynchronous exceptions, e.g., using bracket.
When we get disconnected from the server, we will runReconnectPolicy
to decide what to do next. This can run arbitrary IO actions; two example
use cases are
This can be used to implement a rudimentary redundancy scheme. For
example, you could decide to reconnect to a known fallback server after
connection to a main server fails a certain number of times.
The address of a server to connect to. This is not standard gRPC
nomenclature, but follows convention such as adopted by
grpcurl and
grpc-client-cli, which
distinguish between the address of a server to connect to (hostname and
port), and the (optional) HTTP authority, which is an (optional) string to
be included as the HTTP2
:authoritypseudo-header.
When the authority is not specified, it defaults to addressHost.
This is used both for the HTTP2 :authority pseudo-header as well
as for TLS SNI (if using a secure connection).
Although the HTTP(2) specification allows the authority to include a
port number, and many servers can accept this, this will not work
with TLS, and it is therefore recommended not to include a port number.
Note that the HTTP2 spec explicitly disallows the authority to
include userinfo@.
Instances1Show
ShowAddressDefined in grpc-spec-1.0.0 · Network.GRPC.Spec.Headers.PseudoHeaders
WARNING: This is dangerous. Although communication with the server will
still be encrypted, you cannot be sure that the server is who they claim
to be.
The path may point to single file (multiple PEM formatted certificates
concanated) or directory (one certificate per file, file names are hashes
from certificate).
This is the low-level API for making RPC calls, providing full flexibility.
You may wish to consider using the infrastructure from
Network.GRPC.Client.StreamType.IO instead.
Typical usage:
withRPC conn def (Proxy @ListFeatures) $ \call -> do
.. use 'call' to send and receive messages
for some previously established connection conn
(see withConnection) and where ListFeatures is some
kind of RPC.
The call is setup in the background, and might not yet have been established
when the body is run. If you want to be sure that the call has been setup,
you can call recvResponseMetadata.
There is one exception to this rule: if the server unilaterally closes the
RPC (that is, the server already sent the trailers), then the call is
considered closed and the cancellation exception is not raised. Under normal
circumstances (with well-behaved server handlers) this should not arise.
(The gRPC specification itself is not very specific about this case; see
discussion at https://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client.)
If there are still inbound messages upon leaving the scope of withRPC no
exception is raised (but the call is nonetheless still closed, and the server
handler will be informed that the client has disappeared).
Note on timeouts: if a timeout is specified for the call (either through
callTimeout or through connDefaultTimeout), when the timeout is reached
the RPC is cancelled; any further attempts to receive or send messages will
result in a GrpcException with GrpcDeadlineExceeded. As per the gRPC
specification, this does not rely on the server; this does mean that the
same deadline also applies if the client is slow (rather than the server).
Tell the server that if the request cannot be completed within the
specified amount of time, it should be aborted. If the timeout gets
exceeded, a GrpcDeadlineExceeded exception will
be raised.
Note: the choice of Integer for the result is important: timeouts can be
quite long, and might easily exceed the range of a 32-bit int: 2^31
microseconds is roughly 35 minutes (on 64-bit architectures this is much less
important; 2^63 microseconds is 292,277.2 years). We could use Int64 or
Word64, but Integer works nicely with the unbounded-delays package.
Ongoing calls
3 declarations
Call denotes a previously opened request (see withRPC).
Protobuf communication patterns
This is a general implementation of the
gRPC specification.
As such, these functions do not provide explicit support for the common
communication patterns of non-streaming, server-side streaming, client-side
streaming, or bidirectional streaming. These are not part of the gRPC
standard, but are part of
its Protobuf instantiation, although these
patterns are of course not really Protobuf specific. We provide support for
these communication patterns, independent from a choice of serialization
format, in Network.GRPC.Common.StreamType and
Network.GRPC.Client.StreamType (and Network.GRPC.Server.StreamType for the
server side).
If you only use the abstractions provided in "Network.GRPC.*.StreamType",
you can ignore the rest of the discussion below, which applies only to the
more general interface.
data StreamElem b a =
StreamElem a
| FinalElem a b
| NoMoreElems b
The intuition is that we are sending messages of type a (see "Inputs and
outputs", below) and then when we send the final message, we can include
some additional information of type b (see "Metadata", below).
Inputs and outputs
By convention, we refer to messages sent from the client to the server as
"inputs" and messages sent from the server to the client as "outputs"
(we inherited this terminology from
proto-lens.)
On the client side we therefore have recvOutput and sendInput defined as
The gRPC specification allows the server, but not the client, to include some
final metadata as well; this is the reason between the use of
ResponseTrailingMetadata for messages from the server to the client versus
NoMetadata for messages from the client.
FinalElem versus NoMoreElems
Network.GRPC.Common.StreamElem allows to mark the final message as final
when it is sent (Network.GRPC.Common.FinalElem), or retroactively
indicate that the previous message was in fact final
(Network.GRPC.Common.NoMoreElems). The reason for this is technical in
nature.
Suppose we are doing a grpc+proto non-streaming RPC call. The input message
from the client to the server will be sent over one or more HTTP2 DATA
frames (chunks of the input). The server will expect the last of those frames
to be marked as END_STREAM. The HTTP2 specification does allow sending an
separate empty DATA frame with the END_STREAM flag set to indicate no
further data is coming, but not all gRPC servers will wait for this, and
might either think that the client is broken and disconnect, or might send
the client a RST_STREAM frame to force it to close the stream. To avoid
problems, therefore, it is better to mark the final DATA frame as
END_STREAM; in order to be able to do that, sendInput needs to know
whether an input is the final one. It is therefore better to use FinalElem
instead of NoMoreElems for outgoing messages, if possible.
For incoming messages the situation is different. Now we do expect HTTP
trailers (final metadata), which means that we cannot tell from DATA frames
alone if we have received the last message: it will be the frame containing
the trailers that is marked as END_STREAM, with no indication on the data
frame just before it that it was the last one. We cannot wait for the next
frame to come in, because that would be a blocking call (we might have to
wait for the next TCP packet), and if the output was not the last one, we
would unnecessarily delay making the output we already received available to
the client code. Typically therefore clients will receive a StreamElem
followed by NoMoreElems.
Of course, for a given RPC and its associated communication pattern we may
know whether any given message was the last; in the example above of a
non-streaming grpc+proto RPC call, we only expect a single output. In this
case the client can (and should) call recvOutput again to wait for the
trailers (which, amongst other things, will include the trailerGrpcStatus).
The specialized functions from Network.GRPC.Client.StreamType take care of
this; if these functions are not applicable, users may wish to use
recvFinalOutput.
After the final Output, you will receive any custom metadata (application
defined trailers) that the server returns. We do NOT include the
GrpcStatus here: a status of GrpcOk carries no information, and any other
status will result in a GrpcException. Calling recvOutput again after
receiving the trailers is a bug and results in a RecvAfterFinal exception.
It is however possible for the server to send only a single set; this is
the gRPC "Trailers-Only" case. The server can choose to do so when it knows
it will not send any messages; in this case, the initial response metadata is
fact of type ResponseTrailingMetadata instead. The ResponseMetadata type
distinguishes between these two cases.
If the "Trailers-Only" case can be ruled out (that is, if it would amount
to a protocol error), you can use recvResponseInitialMetadata instead.
This can block: we need to wait until we receive the metadata. The precise
communication pattern will depend on the specifics of each server:
It might be necessary to send one or more inputs to the server before it
returns any replies.
The response metadata will be available before the first output from the
server, and may indeed be available well before.
For some servers it is important that the client marks the final input /when
it is sent/. If you really want to send the final input and separately tell
the server that no more inputs will be provided, use sendEndOfInput (or
sendInput).
This is a specialization of recvResponseMetadata which can be used if a use
of "Trailers-Only" amounts to a protocol error; if the server does use
"Trailers-Only", this throws a ProtoclException
(UnexpectedTrailersOnly).
Information sent by the peer after the final output
Response trailers are a
HTTP2 concept:
they are HTTP headers that are sent after the content body. For example,
imagine the server is streaming a file that it's reading from disk; it could
use trailers to give the client an MD5 checksum when streaming is complete.
This can be used to provide additional details about the RPC error;
as this is a binary field, it can be used for structured data.
The spec imposes some additional restrictions on this field:
Status-Details is allowed only if Status is not OK.
When using Protobuf this contains a google.rpc.Status message.
If it contains a status code (as in the case of a google.rpc.Status
message), it MUST NOT contradict the Status header.
The spec additionally mandates that consumers MUST verify that third
requirement; however, it is impossible to verify this unless a specific
format for the status details is known.
Trailers sent after the response, allowing for invalid trailers
We do not parameterize this over the type of synthesized errors: unlike
response (or request) headers, we have no opportunity to check the trailers
for synthesized errors ahead of time, so having a type to signal
"trailers without synthesized errors" is not particularly useful.
This is a low-level function, and generalizes recvResponseInitialMetadata.
If the server returns a gRPC error, that will be returned as a value here
rather than thrown as an exception.
Most applications will never need to use this function.
Generalization of recvOutput, providing additional meta-information
This returns the full set of trailers, /even if those trailers indicate a
gRPC failure, or if any trailers fail to parse/. Put another way, gRPC
failures are returned as values here, rather than throwing an exception.
Most applications will never need to use this function.