Modeling method calls with requests and responses

Since our service will be exposed through various transport protocols, we will need a way to model the requests and responses in and out of our service. We will do this by adding a struct for each type of message our service will accept or return.

In order for somebody to call the Hash method and then receive the hashed password as a response, we'll need to add the following two structures to service.go:

type hashRequest struct { 
  Password string `json:"password"` 
} 
type hashResponse struct { 
  Hash string `json:"hash"` 
  Err  string `json:"err,omitempty"` 
} 

The hashRequest type contains a single field, the password, and the hashResponse has the resulting hash and an Err string field in case something goes wrong.

Tip

To model remote method calls, you essentially create a struct for the incoming arguments and a struct for the return arguments.

Before continuing, see whether you can model the same request/response pair for the Validate method. Look at the signature in the Service interface, examine the arguments it accepts, and think about what kind of responses it will need to make.

We are going to add a helper method (of type http.DecodeRequestFunc from Go kit) that will be able to decode the JSON body of http.Request to service.go:

func decodeHashRequest(ctx context.Context, r
 *http.Request) (interface{}, error) { 
  var req hashRequest 
  err := json.NewDecoder(r.Body).Decode(&req) 
  if err != nil { 
    return nil, err 
  } 
  return req, nil 
} 

The signature for decodeHashRequest is dictated by Go kit because it will later use it to decode HTTP requests on our behalf. In this function, we just use json.Decoder to unmarshal the JSON into our hashRequest type.

Next, we will add the request and response structures as well as a decode helper function for the Validate method:

type validateRequest struct { 
  Password string `json:"password"` 
  Hash     string `json:"hash"` 
} 
type validateResponse struct { 
  Valid bool   `json:"valid"` 
  Err   string `json:"err,omitempty"` 
} 
func decodeValidateRequest(ctx context.Context, 
 r *http.Request) (interface{}, error) { 
  var req validateRequest 
  err := json.NewDecoder(r.Body).Decode(&req) 
  if err != nil { 
    return nil, err 
  } 
  return req, nil 
} 

Here, the validateRequest struct takes both Password and Hash strings, since the signature has two input arguments and returns a response containing a bool datatype called Valid or Err.

The final thing we need to do is encode the response. In this case, we can write a single method to encode both the hashResponse and validateResponse objects.

Add the following code to service.go:

func encodeResponse(ctx context.Context, 
  w http.ResponseWriter, response interface{})
error { 
  return json.NewEncoder(w).Encode(response) 
} 

Our encodeResponse method just asks json.Encoder to do the work for us. Note again that the signature is general since the response type is interface{}; this is because it's a Go kit mechanism for decoding to http.ResponseWriter.

Endpoints in Go kit

Endpoints are a special function type in Go kit that represent a single RPC method. The definition is inside the endpoint package:

type Endpoint func(ctx context.Context, request
  interface{})  
(response interface{}, err error) 

An endpoint function takes context.Context and request, and it returns response or error. The request and response types are interface{}, which tells us that it is up to the implementation code to deal with the actual types when building endpoints.

Endpoints are powerful because, like http.Handler (and http.HandlerFunc), you can wrap them with generalized middleware to solve a myriad of common issues that arise when building micro-services: logging, tracing, rate limiting, error handling, and more.

Go kit solves transporting over various protocols and uses endpoints as a general way to jump from their code to ours. For example, the gRPC server will listen on a port, and when it receives the appropriate message, it will call the corresponding Endpoint function. Thanks to Go kit, this will all be transparent to us, as we only need to deal in Go code with our Service interface.

Making endpoints for service methods

In order to turn our service methods into endpoint.Endpoint functions, we're going to write a function that handles the incoming hashRequest, calls the Hash service method, and depending on the response, builds and returns an appropriate hashResponse object.

To service.go, add the MakeHashEndpoint function:

func MakeHashEndpoint(srv Service) endpoint.Endpoint { 
  return func(ctx context.Context, request interface{})
  (interface{}, error) { 
    req := request.(hashRequest) 
    v, err := srv.Hash(ctx, req.Password) 
    if err != nil { 
      return hashResponse{v, err.Error()}, nil 
    } 
    return hashResponse{v, ""}, nil 
  } 
} 

This function takes Service as an argument, which means that we can generate an endpoint from any implementation of our Service interface. We then use a type assertion to specify that the request argument should, in fact, be of type hashRequest. We call the Hash method, passing in the context and Password, which we get from hashRequest. If all is well, we build hashResponse with the value we got back from the Hash method and return it.

Let's do the same for the Validate method:

func MakeValidateEndpoint(srv Service) endpoint.Endpoint { 
  return func(ctx context.Context, request interface{})
  (interface{}, error) { 
    req := request.(validateRequest) 
    v, err := srv.Validate(ctx, req.Password, req.Hash) 
    if err != nil { 
      return validateResponse{false, err.Error()}, nil 
    } 
    return validateResponse{v, ""}, nil 
  } 
} 

Here, we are doing the same: taking the request and using it to call the method before building a response. Note that we never return an error from the Endpoint function.

Different levels of error

There are two main types of errors in Go kit: transport errors (network failure, timeouts, dropped connection, and so on) and business logic errors (where the infrastructure of making the request and responding was successful, but something in the logic or data wasn't correct).

If the Hash method returns an error, we are not going to return it as the second argument; instead, we are going to build hashResponse, which contains the error string (accessible via the Error method). This is because the error returned from an endpoint is intended to indicate a transport error, and perhaps Go kit will be configured to retry the call a few times by some middleware. If our service methods return an error, it is considered a business logic error and will probably always return the same error for the same input, so it's not worth retrying. This is why we wrap the error into the response and return it to the client so that they can deal with it.

Wrapping endpoints into a Service implementation

Another very useful trick when dealing with endpoints in Go kit is to write an implementation of our vault.Service interface, which just makes the necessary calls to the underlying endpoints.

To service.go, add the following structure:

type Endpoints struct { 
  HashEndpoint     endpoint.Endpoint 
  ValidateEndpoint endpoint.Endpoint 
} 

In order to implement the vault.Service interface, we are going to add the two methods to our Endpoints structure, which will build a request object, make the request, and parse the resulting response object into the normal arguments to be returned.

Add the following Hash method:

func (e Endpoints) Hash(ctx context.Context, password
  string) (string, error) { 
  req := hashRequest{Password: password} 
  resp, err := e.HashEndpoint(ctx, req) 
  if err != nil { 
    return "", err 
  } 
  hashResp := resp.(hashResponse) 
  if hashResp.Err != "" { 
    return "", errors.New(hashResp.Err) 
  } 
  return hashResp.Hash, nil 
} 

We are calling HashEndpoint with hashRequest, which we create using the password argument before caching the general response to hashResponse and returning the Hash value from it or an error.

We will do this for the Validate method:

func (e Endpoints) Validate(ctx context.Context, password,
 hash string) (bool, error) { 
  req := validateRequest{Password: password, Hash: hash} 
  resp, err := e.ValidateEndpoint(ctx, req) 
  if err != nil { 
    return false, err 
  } 
  validateResp := resp.(validateResponse) 
  if validateResp.Err != "" { 
    return false, errors.New(validateResp.Err) 
  } 
  return validateResp.Valid, nil 
} 

These two methods will allow us to treat the endpoints we have created as though they are normal Go methods; very useful for when we actually consume our service later in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.133.160