-
Notifications
You must be signed in to change notification settings - Fork 912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTTP API support #4543
HTTP API support #4543
Conversation
common/config/config.go
Outdated
// HTTPPort is the port on which HTTP will listen. If unset/0 and HTTP is | ||
// enabled (the default), this will be the gRPC port + 10. This setting only | ||
// applies to the frontend service. | ||
HTTPPort int `yaml:"httpPort"` | ||
// HTTPDisabled can be set to true to disable HTTP API. This setting only | ||
// applies to the frontend service. | ||
HTTPDisabled bool `yaml:"httpDisabled"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatives to httpPort
and httpDisabled
in the frontend
service config directly are to have a top-level HTTP configuration section. However this seemed cleanest.
EDIT: Note, httpDisabled
is no longer present
Details []struct { | ||
RunID string `json:"runId"` | ||
} `json:"details"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this style of details to work, I had to implement a custom error handler because the default reusing our marshaler doesn't work with Google "Any" wkt, and we want to use our marshaler.
_, respBody = s.httpPost( | ||
http.StatusOK, | ||
"/api/v1/namespaces/"+s.namespace+"/workflows/"+workflowID+"/query/some-query", | ||
`{ "query": { "queryArgs": [{ "someField": "query-arg" }] } }`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice the inconsistencies in our API come to the forefront here. What is query.queryArgs
here is input
for signal/workflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are new APIs, though. Why not address this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot address this reasonably with gRPC gateway. Our HTTP API, as a rule and as a byproduct of our approach, exposes the RPCs we have made, it does not alter them. We chose to do this in our API, gRPC and HTTP users have to deal with it. This is covered a bit better in the proposal: https://github.com/temporalio/proposals/blob/master/api/http-api.md.
If we want to fix our RPCs to be more consistent, HTTP and gRPC will benefit. If not, HTTP and gRPC calls will be inconsistent across calls (but consistent across protocols).
// Our version of gRPC gateway only supports integer enums in queries :-( | ||
"/api/v1/namespaces/"+s.namespace+"/workflows/"+workflowID+"/history?historyEventFilterType=2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note how the enum in the query param has to be an integer. This is because our continued use of Gogo proto means we have to support an outdated gRPC gateway which doesn't support enum names in query params. I can customize this, but it's basically copying the entirety of https://github.com/grpc-ecosystem/grpc-gateway/blob/v1.16.0/runtime/query.go and making slight alterations for enum-name support. If I did make this change, it'd be in temporalio/api-go#112.
go.mod
Outdated
@@ -148,3 +149,5 @@ require ( | |||
modernc.org/strutil v1.1.3 // indirect | |||
modernc.org/token v1.1.0 // indirect | |||
) | |||
|
|||
replace go.temporal.io/api => ../temporal-api-go |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is of course temporary pending merge of dependencies
service/frontend/http_api_server.go
Outdated
if tcpAddrRef == nil { | ||
return nil, fmt.Errorf("must use TCP for gRPC listener to support HTTP API") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just use ok
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, I'd include a %T somewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need ok
, why use it? I don't think we need Go types on this error message.
service/frontend/http_api_server.go
Outdated
tcpAddr := *tcpAddrRef | ||
tcpAddr.Port = rpcConfig.HTTPPort | ||
if tcpAddr.Port == 0 { | ||
tcpAddr.Port = tcpAddrRef.Port + 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding 10 here to the RPC port might work for our static configs, but it could conflict with others--it's also just too much implicit behavior IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you suggesting I open on 7243
by default regardless of your gRPC port? I can do that, though would like others' opinions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The +10 feels too magical and surprising to me also. I'd rather just require an explicit port here. All port selection should be done in static config (or by something wrapping the server and generating a static config in memory)
Feel free to add an explicit port to all the development configs and dev servers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we require an explicit port for gRPC today? What does the absence of a gRPC port in config mean today? Does requiring a port basically mean that we are not turning the HTTP server on by default? Or does it mean that we just bind it to any random port by default (arguably a security issue)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do always require an explicit port, afaik. (temporalite has code to pick a random port, but then it puts it in the static config.)
We can set a default port the same way we set ports for everything else: by putting them in all the static config files and docker template static config. (Or have it off by default but I think we decided to have it on by default?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! That's information I didn't know. Yes, I will make this required and set the default in static configs. I assume this is only docker/config_template.yaml
? Can you confirm for me what happens if frontend gRPC port config is left off? (I can try to dig as well, but I assume it picks a random port which makes it not very usable)
EDIT: I will just change our code to assume 0 port (i.e. unset) means HTTP disabled and set it in the docker config and remove explicit HTTP disabling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been fixed to require an HTTP port since we require it for gRPC. The absence of an HTTP port (or 0) means disabled. Docker config templates have been updated to set to 7243 by default.
service/frontend/http_api_server.go
Outdated
} | ||
tcpAddr := *tcpAddrRef | ||
tcpAddr.Port = rpcConfig.HTTPPort | ||
if tcpAddr.Port == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be ok to specify the zero port and get a random available port.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we support that for gRPC port today? How do you differentiate an unset YAML int from 0 with gRPC port today? Or do we actually default to a random port for gRPC? Are we ok w/ default as a random port for HTTP?
service/frontend/http_api_server.go
Outdated
|
||
// 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() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should accept a context because we have one at the top-level from fx (although you may have to plumb it down from there)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use the context for bounding gRPC server graceful stop, why is it a new requirement for HTTP server graceful stop? I intentionally chose to match what gRPC server does.
_, respBody = s.httpPost( | ||
http.StatusOK, | ||
"/api/v1/namespaces/"+s.namespace+"/workflows/"+workflowID+"/query/some-query", | ||
`{ "query": { "queryArgs": [{ "someField": "query-arg" }] } }`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are new APIs, though. Why not address this?
service/frontend/http_api_server.go
Outdated
tcpAddr := *tcpAddrRef | ||
tcpAddr.Port = rpcConfig.HTTPPort | ||
if tcpAddr.Port == 0 { | ||
tcpAddr.Port = tcpAddrRef.Port + 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The +10 feels too magical and surprising to me also. I'd rather just require an explicit port here. All port selection should be done in static config (or by something wrapping the server and generating a static config in memory)
Feel free to add an explicit port to all the development configs and dev servers
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
used named return values and check that instead of an extra local var?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually choose this approach because readers can get confused on when named return values are set and especially if you name it err
it can often be swallowed in confusing ways. But I will change if preferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed about potential confusion with err
: elsewhere in the codebase we use retErr
for that pattern. But no preference here
if conn, _ := r.Context().Value(httpRemoteAddrContextKey{}).(net.Conn); conn != nil { | ||
addr = conn.RemoteAddr() | ||
} | ||
r = r.WithContext(peer.NewContext(r.Context(), &peer.Peer{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice. this means we don't have to change the authorization interceptor at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. And I have tests proving it.
service/frontend/service.go
Outdated
if err := s.server.Serve(listener); err != nil { | ||
logger.Fatal("Failed to serve on frontend listener", tag.Error(err)) | ||
|
||
// Start the gRPC and HTTP server (if any) in the background |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm changing this in #4584, so after that lands, you can just start two goroutines that check return value independently, and not have to do the channel here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. If that lands first I will change when I conflict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that one landed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will merge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, we now do the same for the HTTP server (start in background, don't block)
Added |
service/frontend/fx.go
Outdated
logger log.Logger, | ||
) (*HTTPAPIServer, error) { | ||
// If HTTP API server not enabled, return nil | ||
rpcConfig := config.Services[string(serviceName)].RPC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's no reason internal-frontend
should listen on http. Maybe we should exclude it here too? e.g.
if serviceName != primitives.FrontendService { return nil, nil }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I was not aware there was an internal-frontend reusing this same package/code. Will only apply conditional to ensure only the public frontend gets the HTTP API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
service/frontend/http_api_server.go
Outdated
tcpAddr := *tcpAddrRef | ||
tcpAddr.Port = rpcConfig.HTTPPort | ||
if tcpAddr.Port == 0 { | ||
tcpAddr.Port = tcpAddrRef.Port + 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do always require an explicit port, afaik. (temporalite has code to pick a random port, but then it puts it in the static config.)
We can set a default port the same way we set ports for everything else: by putting them in all the static config files and docker template static config. (Or have it off by default but I think we decided to have it on by default?)
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed about potential confusion with err
: elsewhere in the codebase we use retErr
for that pattern. But no preference here
service/frontend/http_api_server.go
Outdated
// moment using gRPC's default at | ||
// https://github.com/grpc/grpc-go/blob/0673105ebcb956e8bf50b96e28209ab7845a65ad/server.go#L58. | ||
// Max header bytes is defaulted by Go to 1MB. | ||
r.Body = http.MaxBytesReader(w, r.Body, 1024*1024*4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's been talk about increasing this limit for grpc. We already increase it for internode connections but not frontend: https://github.com/temporalio/temporal/blob/master/common/rpc/grpc.go#L58-L59
Can you put this is a constant so we can keep it in sync between grpc and http? Maybe next to that one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. Will call it maxHttpRequestBytes
for now, but would expect it to be updated to share with the gRPC one if/when y'all get around to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
service/frontend/service.go
Outdated
if err := s.server.Serve(listener); err != nil { | ||
logger.Fatal("Failed to serve on frontend listener", tag.Error(err)) | ||
|
||
// Start the gRPC and HTTP server (if any) in the background |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that one landed
@@ -304,6 +304,7 @@ services: | |||
grpcPort: {{ $temporalGrpcPort }} | |||
membershipPort: {{ default .Env.FRONTEND_MEMBERSHIP_PORT "6933" }} | |||
bindOnIP: {{ default .Env.BIND_ON_IP "127.0.0.1" }} | |||
httpPort: {{ default .Env.FRONTEND_HTTP_PORT "7243" }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to check if there are any changes to be made here:
https://github.com/search?q=repo%3Atemporalio%2Fdocker-compose%207233&type=code
or here:
https://github.com/search?q=repo%3Atemporalio%2Fhelm-charts%207233&type=code
to expose the port
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Will open PRs to those upon merge here.
return nil, errRequestIDNotSet | ||
// For easy direct API use, we default the request ID here but expect all | ||
// SDKs and other auto-retrying clients to set it | ||
request.RequestId = uuid.New() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it's worth doing this only for http?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing any call-specific things for HTTP is not only a bit difficult, it violates the spirit of "generic HTTP interface over existing gRPC behavior". So far I have been able to avoid any call-specific HTTP behavior.
# Conflicts: # go.mod # go.sum
What changed?
Implements support for HTTP API as proposed at temporalio/proposals#79. Specifically:
httpPort
andandhttpDisabled
httpAdditionalForwardedHeaders
on config RPCDefault port is gRPC port + 10, defaulthttpDisabled
isfalse
(meaning this is on by default)HTTPAPIServer
for frontend which runs HTTP API server using gRPC gateway with our custom modifications which include?pretty
to pretty print JSON and/or?noPayloadShorthand
to disable shorthand payloadsAny
detailsHTTPAPIServerProvider
for frontend which createsHTTPAPIServer
Service
to supportHTTPAPIServer
This is ready for review. This will not pass CI until the other PRs are merged. Related PRs: