Files
tasks/service/frontend/http_api_server.go
Tim Deeb-SwihartandGitHub dae78a6d83 Rename metricDefinition getters to follow Go's guidelines (#5185)
## What changed?
I renamed the getters on `metricDefinition` to follow go's guidelines.

## Why?
Not only does it save typing, it's better Go.

Quoth [Effective Go](https://go.dev/doc/effective_go#Getters):

> ...it's neither idiomatic nor necessary to put Get into the getter's
name. If you have a field called owner (lower case, unexported), the
getter method should be called Owner (upper case, exported), not
GetOwner.

## How did you test it?
Our current tests are sufficient

## Potential risks
None

## Is hotfix candidate?
No.
2023-11-30 16:34:29 -08:00

480 lines
16 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package frontend
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"reflect"
"time"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/common/config"
"go.temporal.io/server/common/headers"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/rpc"
"go.temporal.io/server/common/rpc/encryption"
"go.temporal.io/server/common/rpc/interceptor"
)
// HTTPAPIServer is an HTTP API server that forwards requests to gRPC via the
// gRPC interceptors.
type HTTPAPIServer struct {
server http.Server
listener net.Listener
logger log.Logger
serveMux *runtime.ServeMux
stopped chan struct{}
matchAdditionalHeaders map[string]bool
}
var defaultForwardedHeaders = []string{
"Authorization-Extras",
"X-Forwarded-For",
http.CanonicalHeaderKey(headers.ClientNameHeaderName),
http.CanonicalHeaderKey(headers.ClientVersionHeaderName),
}
type httpRemoteAddrContextKey struct{}
var (
errHTTPGRPCListenerNotTCP = errors.New("must use TCP for gRPC listener to support HTTP API")
errHTTPGRPCStreamNotSupported = errors.New("stream not supported")
)
// NewHTTPAPIServer creates an [HTTPAPIServer].
func NewHTTPAPIServer(
serviceConfig *Config,
rpcConfig config.RPC,
grpcListener net.Listener,
tlsConfigProvider encryption.TLSConfigProvider,
handler Handler,
interceptors []grpc.UnaryServerInterceptor,
metricsHandler metrics.Handler,
namespaceRegistry namespace.Registry,
logger log.Logger,
) (*HTTPAPIServer, error) {
// Create a TCP listener the same as the frontend one but with different port
tcpAddrRef, _ := grpcListener.Addr().(*net.TCPAddr)
if tcpAddrRef == nil {
return nil, errHTTPGRPCListenerNotTCP
}
tcpAddr := *tcpAddrRef
tcpAddr.Port = rpcConfig.HTTPPort
var listener net.Listener
var err error
if listener, err = net.ListenTCP("tcp", &tcpAddr); err != nil {
return nil, fmt.Errorf("failed listening for HTTP API on %v: %w", &tcpAddr, err)
}
// Close the listener if anything else in this function fails
success := false
defer func() {
if !success {
_ = listener.Close()
}
}()
// Wrap the listener in a TLS listener if there is any TLS config
if tlsConfigProvider != nil {
if tlsConfig, err := tlsConfigProvider.GetFrontendServerConfig(); err != nil {
return nil, fmt.Errorf("failed getting TLS config for HTTP API: %w", err)
} else if tlsConfig != nil {
listener = tls.NewListener(listener, tlsConfig)
}
}
h := &HTTPAPIServer{
listener: listener,
logger: logger,
stopped: make(chan struct{}),
}
// Build 4 possible marshalers in order based on content type
opts := []runtime.ServeMuxOption{
runtime.WithMarshalerOption(newTemporalProtoMarshaler(" ", false)),
runtime.WithMarshalerOption(newTemporalProtoMarshaler("", false)),
runtime.WithMarshalerOption(newTemporalProtoMarshaler(" ", true)),
runtime.WithMarshalerOption(newTemporalProtoMarshaler("", true)),
}
// Set Temporal service error handler
opts = append(opts, runtime.WithErrorHandler(h.errorHandler))
// Match headers w/ default
h.matchAdditionalHeaders = map[string]bool{}
for _, v := range defaultForwardedHeaders {
h.matchAdditionalHeaders[v] = true
}
for _, v := range rpcConfig.HTTPAdditionalForwardedHeaders {
h.matchAdditionalHeaders[http.CanonicalHeaderKey(v)] = true
}
opts = append(opts, runtime.WithIncomingHeaderMatcher(h.incomingHeaderMatcher))
// Create inline client connection
clientConn := newInlineClientConn(
map[string]any{"temporal.api.workflowservice.v1.WorkflowService": handler},
interceptors,
metricsHandler,
namespaceRegistry,
)
// Create serve mux
h.serveMux = runtime.NewServeMux(opts...)
err = workflowservice.RegisterWorkflowServiceHandlerClient(
context.Background(),
h.serveMux,
workflowservice.NewWorkflowServiceClient(clientConn),
)
if err != nil {
return nil, fmt.Errorf("failed registering HTTP API handler: %w", err)
}
// Set the handler as our function that wraps serve mux
h.server.Handler = http.HandlerFunc(h.serveHTTP)
// Put the remote address on the context
h.server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, httpRemoteAddrContextKey{}, c)
}
// We want to set ReadTimeout and WriteTimeout as max idle (and IdleTimeout
// defaults to ReadTimeout) to ensure that a connection cannot hang over that
// amount of time.
h.server.ReadTimeout = serviceConfig.KeepAliveMaxConnectionIdle()
h.server.WriteTimeout = serviceConfig.KeepAliveMaxConnectionIdle()
success = true
return h, nil
}
// Serve serves the HTTP API and does not return until there is a serve error or
// GracefulStop completes. Upon graceful stop, this will return nil. If an error
// is returned, the message is clear that it came from the HTTP API server.
func (h *HTTPAPIServer) Serve() error {
err := h.server.Serve(h.listener)
// If the error is for close, we have to wait for the shutdown to complete and
// we don't consider it an error
if errors.Is(err, http.ErrServerClosed) {
<-h.stopped
err = nil
}
// Wrap the error to be clearer it's from the HTTP API
if err != nil {
return fmt.Errorf("HTTP API serve failed: %w", err)
}
return nil
}
// GracefulStop stops the HTTP server. This will first attempt a graceful stop
// with a drain time, then will hard-stop. This will not return until stopped.
func (h *HTTPAPIServer) GracefulStop(gracefulDrainTime time.Duration) {
// We try a graceful stop for the amount of time we can drain, then we do a
// hard stop
shutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulDrainTime)
defer cancel()
// We intentionally ignore this error, we're gonna stop at this point no
// matter what. This closes the listener too.
_ = h.server.Shutdown(shutdownCtx)
_ = h.server.Close()
close(h.stopped)
}
func (h *HTTPAPIServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
// Limit the request body to max gRPC size. This is hardcoded to 4MB at the
// moment using gRPC's default at
// https://github.com/grpc/grpc-go/blob/0673105ebcb956e8bf50b96e28209ab7845a65ad/server.go#L58
// which is what the constant is set as at the time of this comment.
r.Body = http.MaxBytesReader(w, r.Body, rpc.MaxHTTPAPIRequestBytes)
h.logger.Debug(
"HTTP API call",
tag.NewStringTag("http-method", r.Method),
tag.NewAnyTag("http-url", r.URL),
)
// Need to change the accept header based on whether pretty and/or
// noPayloadShorthand are present
var acceptHeaderSuffix string
if _, ok := r.URL.Query()["pretty"]; ok {
acceptHeaderSuffix += "+pretty"
}
if _, ok := r.URL.Query()["noPayloadShorthand"]; ok {
acceptHeaderSuffix += "+no-payload-shorthand"
}
if acceptHeaderSuffix != "" {
r.Header.Set("Accept", "application/json"+acceptHeaderSuffix)
}
// Put the TLS info on the peer context
if r.TLS != nil {
var addr net.Addr
if conn, _ := r.Context().Value(httpRemoteAddrContextKey{}).(net.Conn); conn != nil {
addr = conn.RemoteAddr()
}
r = r.WithContext(peer.NewContext(r.Context(), &peer.Peer{
Addr: addr,
AuthInfo: credentials.TLSInfo{
State: *r.TLS,
CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity},
},
}))
}
// Call gRPC gateway mux
h.serveMux.ServeHTTP(w, r)
}
func (h *HTTPAPIServer) errorHandler(
ctx context.Context,
mux *runtime.ServeMux,
marshaler runtime.Marshaler,
w http.ResponseWriter,
r *http.Request,
err error,
) {
// Convert the error using serviceerror. The result does not conform to Google
// gRPC status directly (it conforms to gogo gRPC status), but Err() does
// based on internal code reading. However, Err() uses Google proto Any
// which our marshaler is not expecting. So instead we are embedding similar
// logic to runtime.DefaultHTTPProtoErrorHandler in here but with gogo
// support. We don't implement custom content type marshaler or trailers at
// this time.
s := serviceerror.ToStatus(err)
w.Header().Set("Content-Type", marshaler.ContentType(struct{}{}))
buf, merr := marshaler.Marshal(s.Proto())
if merr != nil {
h.logger.Warn("Failed to marshal error message", tag.Error(merr))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"code": 13, "message": "failed to marshal error message"}`))
return
}
w.WriteHeader(runtime.HTTPStatusFromCode(s.Code()))
_, _ = w.Write(buf)
}
func (h *HTTPAPIServer) incomingHeaderMatcher(headerName string) (string, bool) {
// Try ours before falling back to default
if h.matchAdditionalHeaders[headerName] {
return headerName, true
}
return runtime.DefaultHeaderMatcher(headerName)
}
// inlineClientConn is a [grpc.ClientConnInterface] implementation that forwards
// requests directly to gRPC via interceptors. This implementation moves all
// outgoing metadata to incoming and takes resulting outgoing metadata and sets
// as header. But which headers to use and TLS peer context and such are
// expected to be handled by the caller.
type inlineClientConn struct {
methods map[string]*serviceMethod
interceptor grpc.UnaryServerInterceptor
requestsCounter metrics.CounterIface
namespaceRegistry namespace.Registry
}
var _ grpc.ClientConnInterface = (*inlineClientConn)(nil)
type serviceMethod struct {
info grpc.UnaryServerInfo
handler grpc.UnaryHandler
}
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
var protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem()
var errorType = reflect.TypeOf((*error)(nil)).Elem()
func newInlineClientConn(
servers map[string]any,
interceptors []grpc.UnaryServerInterceptor,
metricsHandler metrics.Handler,
namespaceRegistry namespace.Registry,
) *inlineClientConn {
// Create the set of methods via reflection. We currently accept the overhead
// of reflection compared to having to custom generate gateway code.
methods := map[string]*serviceMethod{}
for qualifiedServerName, server := range servers {
serverVal := reflect.ValueOf(server)
for i := 0; i < serverVal.Type().NumMethod(); i++ {
reflectMethod := serverVal.Type().Method(i)
// We intentionally look this up by name to not assume method indexes line
// up from type to value
methodVal := serverVal.MethodByName(reflectMethod.Name)
// We assume the methods we want only accept a context + request and only
// return a response + error. We also assume the method name matches the
// RPC name.
methodType := methodVal.Type()
validRPCMethod := methodType.Kind() == reflect.Func &&
methodType.NumIn() == 2 &&
methodType.NumOut() == 2 &&
methodType.In(0) == contextType &&
methodType.In(1).Implements(protoMessageType) &&
methodType.Out(0).Implements(protoMessageType) &&
methodType.Out(1) == errorType
if !validRPCMethod {
continue
}
fullMethod := "/" + qualifiedServerName + "/" + reflectMethod.Name
methods[fullMethod] = &serviceMethod{
info: grpc.UnaryServerInfo{Server: server, FullMethod: fullMethod},
handler: func(ctx context.Context, req interface{}) (interface{}, error) {
ret := methodVal.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(req)})
err, _ := ret[1].Interface().(error)
return ret[0].Interface(), err
},
}
}
}
return &inlineClientConn{
methods: methods,
interceptor: chainUnaryServerInterceptors(interceptors),
requestsCounter: metricsHandler.Counter(metrics.HTTPServiceRequests.Name()),
namespaceRegistry: namespaceRegistry,
}
}
func (i *inlineClientConn) Invoke(
ctx context.Context,
method string,
args any,
reply any,
opts ...grpc.CallOption,
) error {
// Move outgoing metadata to incoming and set new outgoing metadata
md, _ := metadata.FromOutgoingContext(ctx)
// Set the client and version headers if not already set
if len(md[headers.ClientNameHeaderName]) == 0 {
md.Set(headers.ClientNameHeaderName, headers.ClientNameServerHTTP)
}
if len(md[headers.ClientVersionHeaderName]) == 0 {
md.Set(headers.ClientVersionHeaderName, headers.ServerVersion)
}
ctx = metadata.NewIncomingContext(ctx, md)
outgoingMD := metadata.MD{}
ctx = metadata.NewOutgoingContext(ctx, outgoingMD)
// Get the method. Should never fail, but we check anyways
serviceMethod := i.methods[method]
if serviceMethod == nil {
return status.Error(codes.NotFound, "call not found")
}
// Add metric
var namespaceTag metrics.Tag
if namespaceName := interceptor.MustGetNamespaceName(i.namespaceRegistry, args); namespaceName != "" {
namespaceTag = metrics.NamespaceTag(namespaceName.String())
} else {
namespaceTag = metrics.NamespaceUnknownTag()
}
i.requestsCounter.Record(1, metrics.OperationTag(method), namespaceTag)
// Invoke
var resp any
var err error
if i.interceptor == nil {
resp, err = serviceMethod.handler(ctx, args)
} else {
resp, err = i.interceptor(ctx, args, &serviceMethod.info, serviceMethod.handler)
}
// Find the header call option and set response headers. We accept that if
// somewhere internally the metadata was replaced instead of appended to, this
// does not work.
for _, opt := range opts {
if callOpt, ok := opt.(grpc.HeaderCallOption); ok {
*callOpt.HeaderAddr = outgoingMD
}
}
// Merge the response proto onto the wanted reply if non-nil
if respProto, _ := resp.(proto.Message); respProto != nil {
proto.Merge(reply.(proto.Message), respProto)
}
return err
}
func (*inlineClientConn) NewStream(
context.Context,
*grpc.StreamDesc,
string,
...grpc.CallOption,
) (grpc.ClientStream, error) {
return nil, errHTTPGRPCStreamNotSupported
}
// Mostly taken from https://github.com/grpc/grpc-go/blob/v1.56.1/server.go#L1124-L1158
// with slight modifications.
func chainUnaryServerInterceptors(interceptors []grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
switch len(interceptors) {
case 0:
return nil
case 1:
return interceptors[0]
default:
return chainUnaryInterceptors(interceptors)
}
}
func chainUnaryInterceptors(interceptors []grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler))
}
}
func getChainUnaryHandler(
interceptors []grpc.UnaryServerInterceptor,
curr int,
info *grpc.UnaryServerInfo,
finalHandler grpc.UnaryHandler,
) grpc.UnaryHandler {
if curr == len(interceptors)-1 {
return finalHandler
}
return func(ctx context.Context, req interface{}) (interface{}, error) {
return interceptors[curr+1](ctx, req, info, getChainUnaryHandler(interceptors, curr+1, info, finalHandler))
}
}