Repository: go-kratos/kratos
Pull request #3067 (2023-11-01 06:38:48)

feat(transport/grpc): grpc client supports health check option
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or Draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果 PR 未完成，您可能需要将其标记为 WIP（Work In Progress）PR 或 Draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->


#### Description (what this PR does / why we need it):
Refer to https://github.com/go-kratos/kratos/pull/2736#issue-1625835510 grpc client healthCheck is force to enable.

But in https://github.com/grpc/grpc/blob/master/doc/health-checking.md#grpc-health-checking-protocol, the `Watch()` returns  a stream resp:
```proto
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
```
In some special business usecase，I must use nginx or any other gateway to proxy the grpc request. But it is not support stream resp and it will always return write_timeout. It will take some error in my case like:

`rpc error: code = Unavailable desc = last connection error: connection active but received health check RPC error: rpc error: code = Internal desc = stream terminated by RST_STREAM with error code: INTERNAL_ERROR"`

The nginx plus has impl the health check with https://docs.nginx.com/nginx/admin-guide/load-balancer/grpc-health-check/.
I think it should support an option for user to choice if I want to use the client's health check. And with the https://github.com/go-kratos/kratos/pull/2736#issue-162583551 user should notice it may affects the forward compatibility.

In this pr, it adds a  function `WithHealthCheck()` to disable health check(default is true).


#### Which issue(s) this PR fixes (resolves / be part of):


#### Other special notes for the reviewers:


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/go-kratos/kratos/pull/3067?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#3067](https://app.codecov.io/gh/go-kratos/kratos/pull/3067?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (1a1b6b2) into [main](https://app.codecov.io/gh/go-kratos/kratos/commit/6cdd81811e102620ec93b125ea6ed38f06a5e03c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (6cdd818) will **increase** coverage by `0.02%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 1a1b6b2 differs from pull request most recent head e5bdfaa. Consider uploading reports for the commit e5bdfaa to get more accurate results

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

```diff
@@            Coverage Diff             @@
##             main    #3067      +/-   ##
==========================================
+ Coverage   84.34%   84.36%   +0.02%     
==========================================
  Files          88       88              
  Lines        3986     3992       +6     
==========================================
+ Hits         3362     3368       +6     
  Misses        448      448              
  Partials      176      176              
```


| [Files](https://app.codecov.io/gh/go-kratos/kratos/pull/3067?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Coverage Δ | |
|---|---|---|
| [transport/grpc/client.go](https://app.codecov.io/gh/go-kratos/kratos/pull/3067?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-dHJhbnNwb3J0L2dycGMvY2xpZW50Lmdv) | `84.42% <100.00%> (+0.80%)` | :arrow_up: |



Patch:
diff --git a/transport/grpc/client.go b/transport/grpc/client.go
index 3c8a17d7..d3c24617 100644
--- a/transport/grpc/client.go
+++ b/transport/grpc/client.go
@@ -103,6 +103,15 @@ func WithNodeFilter(filters ...selector.NodeFilter) ClientOption {
 	}
 }
 
+// WithHealthCheck with health check
+func WithHealthCheck(healthCheck bool) ClientOption {
+	return func(o *clientOptions) {
+		if !healthCheck {
+			o.healthCheckConfig = ""
+		}
+	}
+}
+
 // WithLogger with logger
 // Deprecated: use global logger instead.
 func WithLogger(_ log.Logger) ClientOption {
@@ -128,6 +137,7 @@ type clientOptions struct {
 	grpcOpts               []grpc.DialOption
 	balancerName           string
 	filters                []selector.NodeFilter
+	healthCheckConfig      string
 	printDiscoveryDebugLog bool
 }
 
@@ -147,6 +157,7 @@ func dial(ctx context.Context, insecure bool, opts ...ClientOption) (*grpc.Clien
 		balancerName:           balancerName,
 		subsetSize:             25,
 		printDiscoveryDebugLog: true,
+		healthCheckConfig:      `,"healthCheckConfig":{"serviceName":""}`,
 	}
 	for _, o := range opts {
 		o(&options)
@@ -165,7 +176,8 @@ func dial(ctx context.Context, insecure bool, opts ...ClientOption) (*grpc.Clien
 		sints = append(sints, options.streamInts...)
 	}
 	grpcOpts := []grpc.DialOption{
-		grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}],"healthCheckConfig":{"serviceName":""}}`, options.balancerName)),
+		grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]%s}`,
+			options.balancerName, options.healthCheckConfig)),
 		grpc.WithChainUnaryInterceptor(ints...),
 		grpc.WithChainStreamInterceptor(sints...),
 	}


Test patch:
diff --git a/transport/grpc/client_test.go b/transport/grpc/client_test.go
index b2e5e377..2d88f95a 100644
--- a/transport/grpc/client_test.go
+++ b/transport/grpc/client_test.go
@@ -119,6 +119,16 @@ func TestWithOptions(t *testing.T) {
 	}
 }
 
+func TestWithHealthCheck(t *testing.T) {
+	o := &clientOptions{
+		healthCheckConfig: `,"healthCheckConfig":{"serviceName":""}`,
+	}
+	WithHealthCheck(false)(o)
+	if !reflect.DeepEqual("", o.healthCheckConfig) {
+		t.Errorf("expect %v but got %v", "", o.healthCheckConfig)
+	}
+}
+
 func TestDial(t *testing.T) {
 	o := &clientOptions{}
 	v := []grpc.DialOption{