google.golang.org/grpc@v1.72.2/internal/testutils/fakegrpclb/server.go (about) 1 /* 2 * 3 * Copyright 2022 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Package fakegrpclb provides a fake implementation of the grpclb server. 20 package fakegrpclb 21 22 import ( 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/netip" 28 "strconv" 29 "sync" 30 "time" 31 32 "google.golang.org/grpc" 33 lbgrpc "google.golang.org/grpc/balancer/grpclb/grpc_lb_v1" 34 lbpb "google.golang.org/grpc/balancer/grpclb/grpc_lb_v1" 35 "google.golang.org/grpc/codes" 36 "google.golang.org/grpc/grpclog" 37 "google.golang.org/grpc/internal/pretty" 38 "google.golang.org/grpc/status" 39 ) 40 41 var logger = grpclog.Component("fake_grpclb") 42 43 // ServerParams wraps options passed while creating a Server. 44 type ServerParams struct { 45 ListenPort int // Listening port for the balancer server. 46 ServerOptions []grpc.ServerOption // gRPC options for the balancer server. 47 48 LoadBalancedServiceName string // Service name being load balanced for. 49 LoadBalancedServicePort int // Service port being load balanced for. 50 BackendAddresses []string // Service backends to balance load across. 51 ShortStream bool // End balancer stream after sending server list. 52 } 53 54 // Server is a fake implementation of the grpclb LoadBalancer service. It does 55 // not support stats reporting from clients, and always sends back a static list 56 // of backends to the client to balance load across. 57 // 58 // It is safe for concurrent access. 59 type Server struct { 60 lbgrpc.UnimplementedLoadBalancerServer 61 62 // Options copied over from ServerParams passed to NewServer. 63 sOpts []grpc.ServerOption // gRPC server options. 64 serviceName string // Service name being load balanced for. 65 servicePort int // Service port being load balanced for. 66 shortStream bool // End balancer stream after sending server list. 67 68 // Values initialized using ServerParams passed to NewServer. 69 backends []*lbpb.Server // Service backends to balance load across. 70 lis net.Listener // Listener for grpc connections to the LoadBalancer service. 71 72 // mu guards access to below fields. 73 mu sync.Mutex 74 grpcServer *grpc.Server // Underlying grpc server. 75 address string // Actual listening address. 76 77 stopped chan struct{} // Closed when Stop() is called. 78 } 79 80 // NewServer creates a new Server with passed in params. Returns a non-nil error 81 // if the params are invalid. 82 func NewServer(params ServerParams) (*Server, error) { 83 var servers []*lbpb.Server 84 for _, addr := range params.BackendAddresses { 85 ipStr, portStr, err := net.SplitHostPort(addr) 86 if err != nil { 87 return nil, fmt.Errorf("failed to parse list of backend address %q: %v", addr, err) 88 } 89 ip, err := netip.ParseAddr(ipStr) 90 if err != nil { 91 return nil, fmt.Errorf("failed to parse ip %q: %v", ipStr, err) 92 } 93 port, err := strconv.Atoi(portStr) 94 if err != nil { 95 return nil, fmt.Errorf("failed to convert port %q to int", portStr) 96 } 97 logger.Infof("Adding backend ip: %q, port: %d to server list", ip.String(), port) 98 servers = append(servers, &lbpb.Server{ 99 IpAddress: ip.AsSlice(), 100 Port: int32(port), 101 }) 102 } 103 104 lis, err := net.Listen("tcp", "localhost:"+strconv.Itoa(params.ListenPort)) 105 if err != nil { 106 return nil, fmt.Errorf("failed to listen on port %q: %v", params.ListenPort, err) 107 } 108 109 return &Server{ 110 sOpts: params.ServerOptions, 111 serviceName: params.LoadBalancedServiceName, 112 servicePort: params.LoadBalancedServicePort, 113 shortStream: params.ShortStream, 114 backends: servers, 115 lis: lis, 116 address: lis.Addr().String(), 117 stopped: make(chan struct{}), 118 }, nil 119 } 120 121 // Serve starts serving the LoadBalancer service on a gRPC server. 122 // 123 // It returns early with a non-nil error if it is unable to start serving. 124 // Otherwise, it blocks until Stop() is called, at which point it returns the 125 // error returned by the underlying grpc.Server's Serve() method. 126 func (s *Server) Serve() error { 127 s.mu.Lock() 128 if s.grpcServer != nil { 129 s.mu.Unlock() 130 return errors.New("Serve() called multiple times") 131 } 132 133 server := grpc.NewServer(s.sOpts...) 134 s.grpcServer = server 135 s.mu.Unlock() 136 137 logger.Infof("Begin listening on %s", s.lis.Addr().String()) 138 lbgrpc.RegisterLoadBalancerServer(server, s) 139 return server.Serve(s.lis) // This call will block. 140 } 141 142 // Stop stops serving the LoadBalancer service and unblocks the preceding call 143 // to Serve(). 144 func (s *Server) Stop() { 145 defer close(s.stopped) 146 s.mu.Lock() 147 if s.grpcServer != nil { 148 s.grpcServer.Stop() 149 s.grpcServer = nil 150 } 151 s.mu.Unlock() 152 } 153 154 // Address returns the host:port on which the LoadBalancer service is serving. 155 func (s *Server) Address() string { 156 s.mu.Lock() 157 defer s.mu.Unlock() 158 return s.address 159 } 160 161 // BalanceLoad provides a fake implementation of the LoadBalancer service. 162 func (s *Server) BalanceLoad(stream lbgrpc.LoadBalancer_BalanceLoadServer) error { 163 logger.Info("New BalancerLoad stream started") 164 165 req, err := stream.Recv() 166 if err == io.EOF { 167 logger.Warning("Received EOF when reading from the stream") 168 return nil 169 } 170 if err != nil { 171 logger.Warning("Failed to read LoadBalanceRequest from stream: %v", err) 172 return err 173 } 174 logger.Infof("Received LoadBalancerRequest:\n%s", pretty.ToJSON(req)) 175 176 // Initial request contains the service being load balanced for. 177 initialReq := req.GetInitialRequest() 178 if initialReq == nil { 179 logger.Info("First message on the stream does not contain an InitialLoadBalanceRequest") 180 return status.Error(codes.Unknown, "First request not an InitialLoadBalanceRequest") 181 } 182 183 // Basic validation of the service name and port from the incoming request. 184 // 185 // Clients targeting service:port can sometimes include the ":port" suffix in 186 // their requested names; handle this case. 187 serviceName, port, err := net.SplitHostPort(initialReq.Name) 188 if err != nil { 189 // Requested name did not contain a port. So, use the name as is. 190 serviceName = initialReq.Name 191 } else { 192 p, err := strconv.Atoi(port) 193 if err != nil { 194 logger.Info("Failed to parse requested service port %q to integer", port) 195 return status.Error(codes.Unknown, "Bad requested service port number") 196 } 197 if p != s.servicePort { 198 logger.Info("Requested service port number %q does not match expected", port, s.servicePort) 199 return status.Error(codes.Unknown, "Bad requested service port number") 200 } 201 } 202 if serviceName != s.serviceName { 203 logger.Info("Requested service name %q does not match expected %q", serviceName, s.serviceName) 204 return status.Error(codes.NotFound, "Bad requested service name") 205 } 206 207 // Empty initial response disables stats reporting from the client. Stats 208 // reporting from the client is used to determine backend load and is not 209 // required for the purposes of this fake. 210 initResp := &lbpb.LoadBalanceResponse{ 211 LoadBalanceResponseType: &lbpb.LoadBalanceResponse_InitialResponse{ 212 InitialResponse: &lbpb.InitialLoadBalanceResponse{}, 213 }, 214 } 215 if err := stream.Send(initResp); err != nil { 216 logger.Warningf("Failed to send InitialLoadBalanceResponse on the stream: %v", err) 217 return err 218 } 219 220 resp := &lbpb.LoadBalanceResponse{ 221 LoadBalanceResponseType: &lbpb.LoadBalanceResponse_ServerList{ 222 ServerList: &lbpb.ServerList{Servers: s.backends}, 223 }, 224 } 225 logger.Infof("Sending response with server list: %s", pretty.ToJSON(resp)) 226 if err := stream.Send(resp); err != nil { 227 logger.Warningf("Failed to send InitialLoadBalanceResponse on the stream: %v", err) 228 return err 229 } 230 231 if s.shortStream { 232 logger.Info("Ending stream early as the short stream option was set") 233 return nil 234 } 235 236 for { 237 select { 238 case <-stream.Context().Done(): 239 return nil 240 case <-s.stopped: 241 return nil 242 case <-time.After(10 * time.Second): 243 logger.Infof("Sending response with server list: %s", pretty.ToJSON(resp)) 244 if err := stream.Send(resp); err != nil { 245 logger.Warningf("Failed to send InitialLoadBalanceResponse on the stream: %v", err) 246 return err 247 } 248 } 249 } 250 }