go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/grpc.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package server 16 17 import ( 18 "context" 19 "fmt" 20 "net" 21 "strings" 22 "sync" 23 24 "google.golang.org/grpc" 25 "google.golang.org/grpc/health" 26 "google.golang.org/grpc/health/grpc_health_v1" 27 "google.golang.org/grpc/reflection" 28 29 "go.chromium.org/luci/grpc/grpcutil" 30 ) 31 32 // grpcPort implements servingPort on top of a gRPC server. 33 type grpcPort struct { 34 listener net.Listener // the bound socket, always non-nil, doesn't change 35 36 m sync.Mutex // protects mutable state below 37 services []grpcService // services to register when starting 38 opts []grpc.ServerOption // options to pass to the server when starting 39 server *grpc.Server // non-nil after the server has started 40 healthSrv *health.Server // the basic health check service 41 } 42 43 // grpcService is a service registered via registerService. 44 type grpcService struct { 45 desc *grpc.ServiceDesc 46 impl any 47 } 48 49 // registerService exposes the given service over gRPC. 50 func (p *grpcPort) registerService(desc *grpc.ServiceDesc, impl any) { 51 p.m.Lock() 52 defer p.m.Unlock() 53 if p.server != nil { 54 panic("the server has already started") 55 } 56 p.services = append(p.services, grpcService{desc, impl}) 57 } 58 59 // addServerOptions adds an option to pass to NewServer. 60 func (p *grpcPort) addServerOptions(opts ...grpc.ServerOption) { 61 p.m.Lock() 62 defer p.m.Unlock() 63 if p.server != nil { 64 panic("the server has already started") 65 } 66 p.opts = append(p.opts, opts...) 67 } 68 69 // nameForLog returns a string to identify this port in the server logs. 70 // 71 // Part of the servingPort interface. 72 func (p *grpcPort) nameForLog() string { 73 return fmt.Sprintf("grpc://%s [grpc]", p.listener.Addr()) 74 } 75 76 // serve runs the serving loop until it is gracefully stopped. 77 // 78 // Part of the servingPort interface. 79 func (p *grpcPort) serve(baseCtx func() context.Context) error { 80 p.m.Lock() 81 if p.server != nil { 82 panic("the server has already started") 83 } 84 85 // Install outer-most interceptors that append the root server context values 86 // to the per-request context (which is basically just a context.Background() 87 // with its cancellation controlled by the gRPC server). NewServer will append 88 // other interceptors in `opts` after these root ones. 89 injectCtx := contextInjector(baseCtx) 90 p.opts = append(p.opts, 91 grpc.UnaryInterceptor(injectCtx.Unary()), 92 grpc.StreamInterceptor(injectCtx.Stream()), 93 ) 94 p.server = grpc.NewServer(p.opts...) 95 96 // Install reflection only into gRPC server (not pRPC one), since it uses 97 // streaming RPCs not supported by pRPC. pRPC has its own similar service 98 // called Discovery. 99 reflection.Register(p.server) 100 101 // Services installed into both pRPC and gRPC. 102 hasHealth := false 103 for _, svc := range p.services { 104 p.server.RegisterService(svc.desc, svc.impl) 105 if strings.HasPrefix(svc.desc.ServiceName, "grpc.health.") { 106 hasHealth = true 107 } 108 } 109 110 // If the health check service not installed yet by the server user, install 111 // our own basic one. The server users might want to install their own 112 // implementations if they plan to dynamically control the serving status. 113 if !hasHealth { 114 p.healthSrv = health.NewServer() 115 grpc_health_v1.RegisterHealthServer(p.server, p.healthSrv) 116 } 117 118 server := p.server 119 p.m.Unlock() 120 121 return server.Serve(p.listener) 122 } 123 124 // shutdown gracefully stops the server, blocking until it is closed. 125 // 126 // Does nothing if the server is not running. 127 // 128 // Part of the servingPort interface. 129 func (p *grpcPort) shutdown(ctx context.Context) { 130 p.m.Lock() 131 server := p.server 132 if p.healthSrv != nil { 133 p.healthSrv.Shutdown() // announce we are going away 134 } 135 p.m.Unlock() 136 if server != nil { 137 server.GracefulStop() 138 } 139 } 140 141 //////////////////////////////////////////////////////////////////////////////// 142 143 // contextInjector is an interceptor that replaces a context with the one that 144 // takes values from the request context **and** baseCtx(), but keeps 145 // cancellation of the request context. 146 func contextInjector(baseCtx func() context.Context) grpcutil.UnifiedServerInterceptor { 147 return func(ctx context.Context, fullMethod string, handler func(ctx context.Context) error) error { 148 return handler(&mergedCtx{ctx, baseCtx()}) 149 } 150 } 151 152 type mergedCtx struct { 153 context.Context 154 values context.Context 155 } 156 157 func (m mergedCtx) Value(key any) any { 158 if v := m.Context.Value(key); v != nil { 159 return v 160 } 161 return m.values.Value(key) 162 }