Skip to content

Commit 950a7cf

Browse files
authored
health: Add List method to gRPC Health service (#8155)
1 parent 4680429 commit 950a7cf

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

health/server.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ import (
3030
"google.golang.org/grpc/status"
3131
)
3232

33+
const (
34+
// maxAllowedServices defines the maximum number of resources a List
35+
// operation can return. An error is returned if the number of services
36+
// exceeds this limit.
37+
maxAllowedServices = 100
38+
)
39+
3340
// Server implements `service Health`.
3441
type Server struct {
3542
healthgrpc.UnimplementedHealthServer
@@ -62,6 +69,23 @@ func (s *Server) Check(_ context.Context, in *healthpb.HealthCheckRequest) (*hea
6269
return nil, status.Error(codes.NotFound, "unknown service")
6370
}
6471

72+
// List implements `service Health`.
73+
func (s *Server) List(_ context.Context, _ *healthpb.HealthListRequest) (*healthpb.HealthListResponse, error) {
74+
s.mu.RLock()
75+
defer s.mu.RUnlock()
76+
77+
if len(s.statusMap) > maxAllowedServices {
78+
return nil, status.Errorf(codes.ResourceExhausted, "server health list exceeds maximum capacity: %d", maxAllowedServices)
79+
}
80+
81+
statusMap := make(map[string]*healthpb.HealthCheckResponse, len(s.statusMap))
82+
for k, v := range s.statusMap {
83+
statusMap[k] = &healthpb.HealthCheckResponse{Status: v}
84+
}
85+
86+
return &healthpb.HealthListResponse{Statuses: statusMap}, nil
87+
}
88+
6589
// Watch implements `service Health`.
6690
func (s *Server) Watch(in *healthpb.HealthCheckRequest, stream healthgrpc.Health_WatchServer) error {
6791
service := in.Service

health/server_internal_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@
1919
package health
2020

2121
import (
22+
"context"
23+
"errors"
24+
"fmt"
2225
"sync"
2326
"testing"
2427
"time"
2528

29+
"github.com/google/go-cmp/cmp"
30+
"google.golang.org/grpc/codes"
31+
"google.golang.org/grpc/status"
32+
"google.golang.org/protobuf/testing/protocmp"
33+
2634
healthpb "google.golang.org/grpc/health/grpc_health_v1"
2735
"google.golang.org/grpc/internal/grpctest"
2836
)
@@ -81,3 +89,65 @@ func (s) TestShutdown(t *testing.T) {
8189
t.Fatalf("status for %s is %v, want %v", testService, status, healthpb.HealthCheckResponse_NOT_SERVING)
8290
}
8391
}
92+
93+
// TestList verifies that List() returns the health status of all the services if no. of services are within
94+
// maxAllowedLimits.
95+
func (s) TestList(t *testing.T) {
96+
s := NewServer()
97+
98+
// Fill out status map with information
99+
const length = 3
100+
for i := 0; i < length; i++ {
101+
s.SetServingStatus(fmt.Sprintf("%d", i),
102+
healthpb.HealthCheckResponse_ServingStatus(i))
103+
}
104+
105+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
106+
defer cancel()
107+
var in healthpb.HealthListRequest
108+
got, err := s.List(ctx, &in)
109+
110+
if err != nil {
111+
t.Fatalf("s.List(ctx, &in) returned err %v, want nil", err)
112+
}
113+
if len(got.GetStatuses()) != length+1 {
114+
t.Fatalf("len(out.GetStatuses()) = %d, want %d",
115+
len(got.GetStatuses()), length+1)
116+
}
117+
want := &healthpb.HealthListResponse{
118+
Statuses: map[string]*healthpb.HealthCheckResponse{
119+
"": {Status: healthpb.HealthCheckResponse_SERVING},
120+
"0": {Status: healthpb.HealthCheckResponse_UNKNOWN},
121+
"1": {Status: healthpb.HealthCheckResponse_SERVING},
122+
"2": {Status: healthpb.HealthCheckResponse_NOT_SERVING},
123+
},
124+
}
125+
if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" {
126+
t.Fatalf("Health response did not match expectation. Diff (-got, +want): %s", diff)
127+
}
128+
}
129+
130+
// TestListResourceExhausted verifies that List()
131+
// returns a ResourceExhausted error if no. of services are more than
132+
// maxAllowedServices.
133+
func (s) TestListResourceExhausted(t *testing.T) {
134+
s := NewServer()
135+
136+
// Fill out status map with service information,
137+
// 101 (100 + 1 existing) elements will trigger an error.
138+
for i := 1; i <= maxAllowedServices; i++ {
139+
s.SetServingStatus(fmt.Sprintf("%d", i),
140+
healthpb.HealthCheckResponse_SERVING)
141+
}
142+
143+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
144+
defer cancel()
145+
var in healthpb.HealthListRequest
146+
_, err := s.List(ctx, &in)
147+
148+
want := status.Errorf(codes.ResourceExhausted,
149+
"server health list exceeds maximum capacity: %d", maxAllowedServices)
150+
if !errors.Is(err, want) {
151+
t.Fatalf("s.List(ctx, &in) returned %v, want %v", err, want)
152+
}
153+
}

0 commit comments

Comments
 (0)