Skip to content

Commit 3f24eac

Browse files
committed
Add healthcheck for grpc server and client
Signed-off-by: Jian Qiu <jqiu@redhat.com>
1 parent 3fc951c commit 3f24eac

13 files changed

Lines changed: 708 additions & 19 deletions

File tree

pkg/cloudevents/generic/options/grpc/agentoptions.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ package grpc
22

33
import (
44
"context"
5-
65
cloudevents "github.com/cloudevents/sdk-go/v2"
7-
86
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options"
97
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/grpc/protocol"
108
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/types"
@@ -34,11 +32,7 @@ func (o *grpcAgentOptions) WithContext(ctx context.Context, evtCtx cloudevents.E
3432
}
3533

3634
func (o *grpcAgentOptions) Protocol(ctx context.Context, dataType types.CloudEventsDataType) (options.CloudEventsProtocol, error) {
37-
receiver, err := o.GetCloudEventsProtocol(
38-
ctx,
39-
func(err error) {
40-
o.errorChan <- err
41-
},
35+
opts := []protocol.Option{
4236
protocol.WithSubscribeOption(&protocol.SubscribeOption{
4337
// TODO: Update this code to determine the subscription source for the agent client.
4438
// Currently, the grpc agent client is not utilized, and the 'Source' field serves
@@ -47,6 +41,17 @@ func (o *grpcAgentOptions) Protocol(ctx context.Context, dataType types.CloudEve
4741
ClusterName: o.clusterName,
4842
DataType: dataType.String(),
4943
}),
44+
}
45+
if o.ServerHealthinessTimeout != nil {
46+
opts = append(opts, protocol.WithReconnectErrorOption(o.errorChan, *o.ServerHealthinessTimeout))
47+
}
48+
49+
receiver, err := o.GetCloudEventsProtocol(
50+
ctx,
51+
func(err error) {
52+
o.errorChan <- err
53+
},
54+
opts...,
5055
)
5156
if err != nil {
5257
return nil, err

pkg/cloudevents/generic/options/grpc/options.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ func (d *GRPCDialer) Close() error {
114114
// GRPCOptions holds the options that are used to build gRPC client.
115115
type GRPCOptions struct {
116116
Dialer *GRPCDialer
117+
118+
ServerHealthinessTimeout *time.Duration
117119
}
118120

119121
// GRPCConfig holds the information needed to build connect to gRPC server as a given user.
@@ -130,6 +132,10 @@ type GRPCConfig struct {
130132

131133
// keepalive options
132134
KeepAliveConfig KeepAliveConfig `json:"keepAliveConfig,omitempty" yaml:"keepAliveConfig,omitempty"`
135+
136+
// serverHealthinessTimeout is the max duration that client will reconnect if no health status message is
137+
// received in this duration
138+
ServerHealthinessTimeout *time.Duration `json:"serverHealthinessTimeout,omitempty" yaml:"serverHealthinessTimeout,omitempty"`
133139
}
134140

135141
// KeepAliveConfig holds the keepalive options for the gRPC client.
@@ -200,6 +206,7 @@ func BuildGRPCOptionsFromFlags(configPath string) (*GRPCOptions, error) {
200206
URL: config.URL,
201207
Token: token,
202208
},
209+
ServerHealthinessTimeout: config.ServerHealthinessTimeout,
203210
}
204211

205212
// Default keepalive options

pkg/cloudevents/generic/options/grpc/protocol/message.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const (
1919
prefix = "ce-"
2020
contenttype = "contenttype"
2121
// dataSchema = "dataschema"
22-
subject = "subject"
23-
time = "time"
22+
subject = "subject"
23+
timestamp = "time"
2424
)
2525

2626
var specs = spec.WithPrefix(prefix)

pkg/cloudevents/generic/options/grpc/protocol/option.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package protocol
22

33
import (
44
"fmt"
5+
"time"
56
)
67

78
// Option is the function signature
@@ -24,3 +25,18 @@ func WithSubscribeOption(subscribeOpt *SubscribeOption) Option {
2425
return nil
2526
}
2627
}
28+
29+
func WithReconnectErrorOption(reconnectError chan error, interval time.Duration) Option {
30+
return func(p *Protocol) error {
31+
if reconnectError == nil {
32+
return fmt.Errorf("the reconnect error option must not be nil")
33+
}
34+
p.reconnectErrorChan = reconnectError
35+
if interval <= 0 {
36+
p.checkInterval = 20 * time.Second
37+
} else {
38+
p.checkInterval = interval
39+
}
40+
return nil
41+
}
42+
}

pkg/cloudevents/generic/options/grpc/protocol/protocol.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package protocol
33
import (
44
"context"
55
"fmt"
6+
healthpb "google.golang.org/grpc/health/grpc_health_v1"
67
"io"
78
"sync"
9+
"time"
810

911
"google.golang.org/grpc"
1012
"google.golang.org/grpc/codes"
@@ -30,6 +32,10 @@ type Protocol struct {
3032
openerMutex sync.Mutex
3133

3234
closeChan chan struct{}
35+
36+
// errorChan is to send an error messsage to restart the connection
37+
reconnectErrorChan chan error
38+
checkInterval time.Duration
3339
}
3440

3541
var (
@@ -137,6 +143,10 @@ func (p *Protocol) OpenInbound(ctx context.Context) error {
137143
}
138144
}()
139145

146+
if p.reconnectErrorChan != nil {
147+
go p.healthCheck(ctx)
148+
}
149+
140150
// Wait until external or internal context done
141151
select {
142152
case <-ctx.Done():
@@ -164,3 +174,58 @@ func (p *Protocol) Close(ctx context.Context) error {
164174
close(p.closeChan)
165175
return nil
166176
}
177+
178+
// healthCheck check status of the server
179+
func (p *Protocol) healthCheck(ctx context.Context) {
180+
logger := cecontext.LoggerFrom(ctx)
181+
watchCtx, cancel := context.WithCancel(ctx)
182+
defer cancel()
183+
184+
healthClient := healthpb.NewHealthClient(p.clientConn)
185+
stream, err := healthClient.Watch(watchCtx, &healthpb.HealthCheckRequest{Service: ""})
186+
if err != nil {
187+
select {
188+
case p.reconnectErrorChan <- err:
189+
default:
190+
}
191+
return
192+
}
193+
194+
last := time.Now()
195+
recvErr := make(chan error, 1)
196+
go func() {
197+
for {
198+
resp, err := stream.Recv()
199+
if err != nil {
200+
recvErr <- err
201+
return
202+
}
203+
last = time.Now()
204+
logger.Infof("Received server health status %s", resp.Status)
205+
}
206+
}()
207+
208+
ticker := time.NewTicker(p.checkInterval)
209+
defer ticker.Stop()
210+
211+
for {
212+
select {
213+
case <-ctx.Done():
214+
return
215+
case err := <-recvErr:
216+
select {
217+
case p.reconnectErrorChan <- err:
218+
default:
219+
}
220+
return
221+
case <-ticker.C:
222+
if time.Since(last) > p.checkInterval {
223+
select {
224+
case p.reconnectErrorChan <- fmt.Errorf("timeout waiting for health check"):
225+
default:
226+
}
227+
return
228+
}
229+
}
230+
}
231+
}

pkg/cloudevents/generic/options/grpc/protocol/write_message.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ func (b *pbEventWriter) SetAttribute(attribute spec.Attribute, value interface{}
136136
}
137137
case spec.Time:
138138
if value == nil {
139-
delete(b.Attributes, prefix+time)
139+
delete(b.Attributes, prefix+timestamp)
140140
} else {
141141
attrVal, err := attributeFor(value)
142142
if err != nil {
143143
return err
144144
}
145-
b.Attributes[prefix+time] = attrVal
145+
b.Attributes[prefix+timestamp] = attrVal
146146
}
147147
default:
148148
if value == nil {

pkg/cloudevents/generic/options/grpc/sourceoptions.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package grpc
22

33
import (
44
"context"
5-
65
cloudevents "github.com/cloudevents/sdk-go/v2"
76

87
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options"
@@ -33,15 +32,22 @@ func (o *gRPCSourceOptions) WithContext(ctx context.Context, evtCtx cloudevents.
3332
}
3433

3534
func (o *gRPCSourceOptions) Protocol(ctx context.Context, dataType types.CloudEventsDataType) (options.CloudEventsProtocol, error) {
35+
opts := []protocol.Option{
36+
protocol.WithSubscribeOption(&protocol.SubscribeOption{
37+
Source: o.sourceID,
38+
DataType: dataType.String(),
39+
}),
40+
}
41+
if o.ServerHealthinessTimeout != nil {
42+
opts = append(opts, protocol.WithReconnectErrorOption(o.errorChan, *o.ServerHealthinessTimeout))
43+
}
44+
3645
receiver, err := o.GetCloudEventsProtocol(
3746
ctx,
3847
func(err error) {
3948
o.errorChan <- err
4049
},
41-
protocol.WithSubscribeOption(&protocol.SubscribeOption{
42-
Source: o.sourceID,
43-
DataType: dataType.String(),
44-
}),
50+
opts...,
4551
)
4652
if err != nil {
4753
return nil, err

pkg/server/grpc/health/health.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package health
2+
3+
import (
4+
"context"
5+
"google.golang.org/grpc"
6+
"time"
7+
8+
healthpb "google.golang.org/grpc/health/grpc_health_v1"
9+
)
10+
11+
// Custom health server with periodic broadcasts
12+
type heartbeatHealthServer struct {
13+
interval time.Duration
14+
healthpb.UnimplementedHealthServer
15+
status healthpb.HealthCheckResponse_ServingStatus
16+
}
17+
18+
func RegisterHeartbeatHealthServer(srv *grpc.Server, interval time.Duration) {
19+
healthpb.RegisterHealthServer(srv, &heartbeatHealthServer{
20+
interval: interval,
21+
})
22+
}
23+
24+
func (s *heartbeatHealthServer) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
25+
return &healthpb.HealthCheckResponse{Status: s.status}, nil
26+
}
27+
28+
func (s *heartbeatHealthServer) Watch(req *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error {
29+
ticker := time.NewTicker(s.interval) // send every 5s
30+
defer ticker.Stop()
31+
32+
for {
33+
select {
34+
case <-ticker.C:
35+
if err := stream.Send(&healthpb.HealthCheckResponse{Status: s.status}); err != nil {
36+
return err
37+
}
38+
case <-stream.Context().Done():
39+
return stream.Context().Err()
40+
}
41+
}
42+
}

pkg/server/grpc/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type GRPCServerOptions struct {
3030
ServerPingInterval time.Duration `json:"server_ping_interval" yaml:"server_ping_interval"`
3131
ServerPingTimeout time.Duration `json:"server_ping_timeout" yaml:"server_ping_timeout"`
3232
PermitPingWithoutStream bool `json:"permit_ping_without_stream" yaml:"permit_ping_without_stream"`
33+
HealthCheckInterval time.Duration `json:"health_check_interval" yaml:"health_check_interval"`
3334
}
3435

3536
func LoadGRPCServerOptions(configPath string) (*GRPCServerOptions, error) {
@@ -73,6 +74,7 @@ func NewGRPCServerOptions() *GRPCServerOptions {
7374
ServerPingTimeout: 10 * time.Second,
7475
WriteBufferSize: 32 * 1024,
7576
ReadBufferSize: 32 * 1024,
77+
HealthCheckInterval: 10 * time.Second,
7678
}
7779
}
7880

@@ -92,6 +94,7 @@ func (o *GRPCServerOptions) AddFlags(flags *pflag.FlagSet) {
9294
flags.StringVar(&o.TLSCertFile, "grpc-tls-cert-file", o.TLSCertFile, "The path to the tls.crt file")
9395
flags.StringVar(&o.TLSKeyFile, "grpc-tls-key-file", o.TLSKeyFile, "The path to the tls.key file")
9496
flags.StringVar(&o.ClientCAFile, "grpc-client-ca-file", o.ClientCAFile, "The path to the client ca file, must specify if using mtls authentication type")
97+
flags.DurationVar(&o.HealthCheckInterval, "grpc-healtch-check-interval", o.HealthCheckInterval, "The interval at which health status message is sent")
9598
}
9699

97100
// Validate checks option ranges and cross-field constraints.

pkg/server/grpc/server.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import (
55
"crypto/tls"
66
"crypto/x509"
77
"fmt"
8-
"net"
9-
"os"
10-
118
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
129
"google.golang.org/grpc"
1310
"google.golang.org/grpc/credentials"
1411
"google.golang.org/grpc/keepalive"
1512
"k8s.io/apimachinery/pkg/util/errors"
1613
k8smetrics "k8s.io/component-base/metrics"
14+
"net"
1715
"open-cluster-management.io/sdk-go/pkg/server/grpc/authn"
1816
"open-cluster-management.io/sdk-go/pkg/server/grpc/authz"
17+
"open-cluster-management.io/sdk-go/pkg/server/grpc/health"
1918
"open-cluster-management.io/sdk-go/pkg/server/grpc/metrics"
19+
"os"
2020

2121
"k8s.io/klog/v2"
2222
)
@@ -133,6 +133,8 @@ func (b *GRPCServer) Run(ctx context.Context) error {
133133
metrics.RegisterGRPCMetrics(promMiddleware, b.extraMetrics...)
134134
// initialize grpc server metrics with appropriate value.
135135
promMiddleware.InitializeMetrics(grpcServer)
136+
// register health server
137+
health.RegisterHeartbeatHealthServer(grpcServer, b.options.HealthCheckInterval)
136138

137139
for _, r := range b.registerFuncs {
138140
r(grpcServer)

0 commit comments

Comments
 (0)