go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/span/module.go (about) 1 // Copyright 2020 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 span implements a server module for communicating with Cloud Spanner. 16 package span 17 18 import ( 19 "context" 20 "flag" 21 "strings" 22 "time" 23 24 "cloud.google.com/go/spanner" 25 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 26 "google.golang.org/api/option" 27 "google.golang.org/grpc" 28 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/grpc/grpcmon" 31 32 "go.chromium.org/luci/server/auth" 33 "go.chromium.org/luci/server/module" 34 ) 35 36 // ModuleName can be used to refer to this module when declaring dependencies. 37 var ModuleName = module.RegisterName("go.chromium.org/luci/server/span") 38 39 // ClientConfigProvider supplies custom Cloud Spanner client config. 40 // 41 // This callback is called right before constructing the spanner client. 42 type ClientConfigProvider func(ctx context.Context, opts module.HostOptions) (spanner.ClientConfig, error) 43 44 // ModuleOptions contain configuration of the Cloud Spanner server module. 45 type ModuleOptions struct { 46 SpannerEndpoint string // the Spanner endpoint to connect to 47 SpannerDatabase string // identifier of Cloud Spanner database to connect to 48 ClientConfig ClientConfigProvider // if set, use the provided client config 49 } 50 51 // Register registers the command line flags. 52 func (o *ModuleOptions) Register(f *flag.FlagSet) { 53 f.StringVar( 54 &o.SpannerEndpoint, 55 "spanner-endpoint", 56 o.SpannerEndpoint, 57 "The Spanner endpoint to connect to. "+ 58 "The default is defined by the Cloud Spanner library, "+ 59 "but usually it is spanner.googleapis.com:443", 60 ) 61 f.StringVar( 62 &o.SpannerDatabase, 63 "spanner-database", 64 o.SpannerDatabase, 65 "Identifier of the Cloud Spanner database to connect to. A valid database "+ 66 "name has the form projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID. Required.", 67 ) 68 } 69 70 // NewModule returns a server module that sets up a Spanner client connected 71 // to some single Cloud Spanner database. 72 // 73 // Client's functionality is exposed via Single(ctx), ReadOnlyTransaction(ctx), 74 // ReadWriteTransaction(ctx), etc. 75 // 76 // The underlying *spanner.Client is intentionally not exposed to make sure 77 // all callers use the functions mentioned above since they generally add 78 // additional functionality on top of the raw Spanner client that other LUCI 79 // packages assume to be present. Using the Spanner client directly may violate 80 // such assumptions leading to undefined behavior when multiple packages are 81 // used together. 82 func NewModule(opts *ModuleOptions) module.Module { 83 if opts == nil { 84 opts = &ModuleOptions{} 85 } 86 return &spannerModule{opts: opts} 87 } 88 89 // NewModuleFromFlags is a variant of NewModule that initializes applicable 90 // options through command line flags. 91 // 92 // Calling this function registers flags in flag.CommandLine. They are usually 93 // parsed in server.Main(...). 94 // 95 // If given a non-nil ClientConfigProvider callback, it will be called when 96 // creating the Cloud Spanner client to get a custom spanner.ClientConfig. 97 // This can be used to set custom retry policies and timeouts, see 98 // https://cloud.google.com/spanner/docs/custom-timeout-and-retry. 99 func NewModuleFromFlags(cfg ClientConfigProvider) module.Module { 100 opts := &ModuleOptions{ClientConfig: cfg} 101 opts.Register(flag.CommandLine) 102 return NewModule(opts) 103 } 104 105 // spannerModule implements module.Module. 106 type spannerModule struct { 107 opts *ModuleOptions 108 } 109 110 // Name is part of module.Module interface. 111 func (*spannerModule) Name() module.Name { 112 return ModuleName 113 } 114 115 // Dependencies is part of module.Module interface. 116 func (*spannerModule) Dependencies() []module.Dependency { 117 return nil 118 } 119 120 // Initialize is part of module.Module interface. 121 func (m *spannerModule) Initialize(ctx context.Context, host module.Host, opts module.HostOptions) (context.Context, error) { 122 switch { 123 case m.opts.SpannerDatabase == "": 124 return nil, errors.New("Cloud Spanner database name is required") 125 case !isValidDB(m.opts.SpannerDatabase): 126 return nil, errors.New("Cloud Spanner database name must have form `projects/.../instances/.../databases/...`") 127 } 128 129 // Credentials with Cloud scope. 130 creds, err := auth.GetPerRPCCredentials(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 131 if err != nil { 132 return nil, errors.Annotate(err, "failed to get PerRPCCredentials").Err() 133 } 134 135 // Figure out what client config to use. 136 var cfg spanner.ClientConfig 137 if m.opts.ClientConfig != nil { 138 if cfg, err = m.opts.ClientConfig(ctx, opts); err != nil { 139 return nil, errors.Annotate(err, "failed to get custom ClientConfig").Err() 140 } 141 } else { 142 cfg = spanner.ClientConfig{ 143 SessionPoolConfig: spanner.SessionPoolConfig{ 144 TrackSessionHandles: !opts.Prod, 145 }, 146 } 147 } 148 149 // Initialize the client. 150 options := []option.ClientOption{ 151 option.WithGRPCDialOption(grpc.WithPerRPCCredentials(creds)), 152 option.WithGRPCDialOption(grpc.WithStatsHandler(&grpcmon.ClientRPCStatsMonitor{})), 153 option.WithGRPCDialOption(grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor())), 154 option.WithGRPCDialOption(grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor())), 155 } 156 if m.opts.SpannerEndpoint != "" { 157 options = append(options, option.WithEndpoint(m.opts.SpannerEndpoint)) 158 } 159 client, err := spanner.NewClientWithConfig(ctx, m.opts.SpannerDatabase, cfg, options...) 160 if err != nil { 161 return nil, errors.Annotate(err, "failed to instantiate Cloud Spanner client").Err() 162 } 163 ctx = UseClient(ctx, client) 164 165 // Close the client when exiting gracefully. 166 host.RegisterCleanup(func(ctx context.Context) { client.Close() }) 167 168 // Run a "select 1" query to verify the database exists and we can access it 169 // before we actually serve any requests. 170 if err := pingDB(ctx); err != nil { 171 return nil, errors.Annotate(err, "failed to ping Cloud Spanner database").Err() 172 } 173 174 return ctx, nil 175 } 176 177 func isValidDB(name string) bool { 178 chunks := strings.Split(name, "/") 179 if len(chunks) != 6 { 180 return false 181 } 182 for _, ch := range chunks { 183 if ch == "" { 184 return false 185 } 186 } 187 return chunks[0] == "projects" && chunks[2] == "instances" && chunks[4] == "databases" 188 } 189 190 func pingDB(ctx context.Context) error { 191 ctx, done := context.WithTimeout(Single(ctx), 30*time.Second) 192 defer done() 193 return Query(ctx, spanner.NewStatement("SELECT 1;")).Do(func(*spanner.Row) error { return nil }) 194 }