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  }