Skip to content

Commit 0ac0d26

Browse files
nimfrahul2393
andauthored
feat(spanner): support MultiEndpoint (#9565)
* feat(spanner): MultiEndpoint support * update spanner go.mod --------- Co-authored-by: rahul2393 <[email protected]>
1 parent 84e3236 commit 0ac0d26

14 files changed

+2180
-61
lines changed

go.work.sum

+33
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
cloud.google.com/go/auth v0.2.0/go.mod h1:+yb+oy3/P0geX6DLKlqiGHARGR6EX2GRtYCzWOCQSbU=
22
cloud.google.com/go/auth/oauth2adapt v0.2.0/go.mod h1:AfqujpDAlTfLfeCIl/HJZZlIxD8+nJoZ5e0x1IxGq5k=
33
cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU=
4+
cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc=
5+
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE=
6+
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
7+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.18.0 h1:ugYJK/neZQtQeh2jc5xNoDFiMQojlAkoqJMRb7vTu1U=
8+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.18.0/go.mod h1:Xx0VKh7GJ4si3rmElbh19Mejxz68ibWg/J30ZOMrqzU=
49
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.46.0/go.mod h1:V28hx+cUCZC9e3qcqszMb+Sbt8cQZtHTiXOmyDzoDOg=
510
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
611
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
@@ -31,6 +36,7 @@ github.com/fullstorydev/grpcurl v1.8.7/go.mod h1:pVtM4qe3CMoLaIzYS8uvTuDj2jVYmXq
3136
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
3237
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
3338
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
39+
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
3440
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
3541
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
3642
github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA=
@@ -51,6 +57,33 @@ github.com/miekg/dns v1.1.33/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7
5157
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
5258
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
5359
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
60+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
61+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
62+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
63+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
64+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
65+
go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
66+
go.opentelemetry.io/otel/bridge/opencensus v0.40.0 h1:pqDiayRhBgoqy1vwnscik+TizcImJ58l053NScJyZso=
67+
go.opentelemetry.io/otel/bridge/opencensus v0.40.0/go.mod h1:1NvVHb6tLTe5A9qCYz+eErW0t8iPn4ZfR6tDKcqlGTM=
68+
go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
69+
go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
70+
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
71+
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
72+
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
73+
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
74+
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
75+
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
76+
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
77+
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
78+
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
79+
google.golang.org/genproto v0.0.0-20230725213213-b022f6e96895/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
80+
google.golang.org/genproto/googleapis/api v0.0.0-20230725213213-b022f6e96895/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
81+
google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f/go.mod h1:iIgEblxoG4klcXsG0d9cpoxJ4xndv6+1FkDROCHhPRI=
82+
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240102182953-50ed04b92917/go.mod h1:O9TvT7A9NLgdqqF0JJXJ+axpaoYiEb8txGmkvy+AvLc=
83+
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8=
84+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
85+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228201840-1f18d85a4ec2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
86+
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
5487
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
5588
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
5689
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

spanner/client.go

+192-9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828

2929
"cloud.google.com/go/internal/trace"
3030
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
31+
"github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp"
32+
grpcgcppb "github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp/grpc_gcp"
3133
"github.com/googleapis/gax-go/v2"
3234
"go.opentelemetry.io/otel/attribute"
3335
"go.opentelemetry.io/otel/metric"
@@ -37,6 +39,7 @@ import (
3739
gtransport "google.golang.org/api/transport/grpc"
3840
"google.golang.org/grpc"
3941
"google.golang.org/grpc/codes"
42+
"google.golang.org/grpc/credentials/insecure"
4043
"google.golang.org/grpc/encoding/gzip"
4144
"google.golang.org/grpc/metadata"
4245

@@ -121,6 +124,131 @@ func (c *Client) ClientID() string {
121124
return c.sc.id
122125
}
123126

127+
func createGCPMultiEndpoint(cfg *grpcgcp.GCPMultiEndpointOptions, config ClientConfig, opts ...option.ClientOption) (*grpcgcp.GCPMultiEndpoint, error) {
128+
if cfg.GRPCgcpConfig == nil {
129+
cfg.GRPCgcpConfig = &grpcgcppb.ApiConfig{}
130+
}
131+
if cfg.GRPCgcpConfig.Method == nil || len(cfg.GRPCgcpConfig.Method) == 0 {
132+
cfg.GRPCgcpConfig.Method = []*grpcgcppb.MethodConfig{
133+
{
134+
Name: []string{"/google.spanner.v1.Spanner/CreateSession"},
135+
Affinity: &grpcgcppb.AffinityConfig{
136+
Command: grpcgcppb.AffinityConfig_BIND,
137+
AffinityKey: "name",
138+
},
139+
},
140+
{
141+
Name: []string{"/google.spanner.v1.Spanner/BatchCreateSessions"},
142+
Affinity: &grpcgcppb.AffinityConfig{
143+
Command: grpcgcppb.AffinityConfig_BIND,
144+
AffinityKey: "session.name",
145+
},
146+
},
147+
{
148+
Name: []string{"/google.spanner.v1.Spanner/DeleteSession"},
149+
Affinity: &grpcgcppb.AffinityConfig{
150+
Command: grpcgcppb.AffinityConfig_UNBIND,
151+
AffinityKey: "name",
152+
},
153+
},
154+
{
155+
Name: []string{"/google.spanner.v1.Spanner/GetSession"},
156+
Affinity: &grpcgcppb.AffinityConfig{
157+
Command: grpcgcppb.AffinityConfig_BOUND,
158+
AffinityKey: "name",
159+
},
160+
},
161+
{
162+
Name: []string{
163+
"/google.spanner.v1.Spanner/BeginTransaction",
164+
"/google.spanner.v1.Spanner/Commit",
165+
"/google.spanner.v1.Spanner/ExecuteBatchDml",
166+
"/google.spanner.v1.Spanner/ExecuteSql",
167+
"/google.spanner.v1.Spanner/ExecuteStreamingSql",
168+
"/google.spanner.v1.Spanner/PartitionQuery",
169+
"/google.spanner.v1.Spanner/PartitionRead",
170+
"/google.spanner.v1.Spanner/Read",
171+
"/google.spanner.v1.Spanner/Rollback",
172+
"/google.spanner.v1.Spanner/StreamingRead",
173+
},
174+
Affinity: &grpcgcppb.AffinityConfig{
175+
Command: grpcgcppb.AffinityConfig_BOUND,
176+
AffinityKey: "session",
177+
},
178+
},
179+
}
180+
}
181+
// Append emulator options if SPANNER_EMULATOR_HOST has been set.
182+
if emulatorAddr := os.Getenv("SPANNER_EMULATOR_HOST"); emulatorAddr != "" {
183+
emulatorOpts := []option.ClientOption{
184+
option.WithEndpoint(emulatorAddr),
185+
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
186+
option.WithoutAuthentication(),
187+
internaloption.SkipDialSettingsValidation(),
188+
}
189+
opts = append(opts, emulatorOpts...)
190+
// Replace all endpoints with emulator target.
191+
for _, meo := range cfg.MultiEndpoints {
192+
meo.Endpoints = []string{emulatorAddr}
193+
}
194+
}
195+
196+
// Set the number of channels to the default value if not specified.
197+
if cfg.GRPCgcpConfig.GetChannelPool() == nil || cfg.GRPCgcpConfig.GetChannelPool().GetMaxSize() == 0 {
198+
cfg.GRPCgcpConfig.ChannelPool = &grpcgcppb.ChannelPoolConfig{
199+
MinSize: numChannels,
200+
MaxSize: numChannels,
201+
}
202+
}
203+
// Set MinSize equal to MaxSize to create all the channels beforehand.
204+
cfg.GRPCgcpConfig.ChannelPool.MinSize = cfg.GRPCgcpConfig.ChannelPool.GetMaxSize()
205+
206+
cfg.GRPCgcpConfig.ChannelPool.BindPickStrategy = grpcgcppb.ChannelPoolConfig_ROUND_ROBIN
207+
208+
cfg.DialFunc = func(ctx context.Context, target string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
209+
copts := opts
210+
211+
for _, do := range dopts {
212+
copts = append(copts, option.WithGRPCDialOption(do))
213+
}
214+
215+
allOpts := allClientOpts(1, config.Compression, copts...)
216+
217+
// Overwrite endpoint and pool config.
218+
allOpts = append(allOpts,
219+
option.WithEndpoint(target),
220+
option.WithGRPCConnectionPool(1),
221+
option.WithGRPCConn(nil),
222+
)
223+
224+
return gtransport.Dial(ctx, allOpts...)
225+
}
226+
227+
gme, err := grpcgcp.NewGCPMultiEndpoint(cfg)
228+
return gme, err
229+
}
230+
231+
// To use GCPMultiEndpoint in gtransport.Dial (via gtransport.WithConnPool option)
232+
// we implement gtransport.ConnPool interface using this wrapper.
233+
type gmeWrapper struct {
234+
*grpcgcp.GCPMultiEndpoint
235+
}
236+
237+
// Make sure gmeWrapper implements ConnPool interface.
238+
var _ gtransport.ConnPool = (*gmeWrapper)(nil)
239+
240+
func (gw *gmeWrapper) Conn() *grpc.ClientConn {
241+
// GCPMultiEndpoint does not expose any ClientConn.
242+
// This is safe because Cloud Spanner client doesn't use this function and instead
243+
// makes calls directly using Invoke and NewStream from the grpc.ClientConnInterface
244+
// which GCPMultiEndpoint implements.
245+
return nil
246+
}
247+
248+
func (gw *gmeWrapper) Num() int {
249+
return int(gw.GCPMultiEndpoint.GCPConfig().GetChannelPool().GetMaxSize())
250+
}
251+
124252
// ClientConfig has configurations for the client.
125253
type ClientConfig struct {
126254
// NumChannels is the number of gRPC channels.
@@ -241,6 +369,10 @@ func NewClient(ctx context.Context, database string, opts ...option.ClientOption
241369
// NewClientWithConfig creates a client to a database. A valid database name has
242370
// the form projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID.
243371
func NewClientWithConfig(ctx context.Context, database string, config ClientConfig, opts ...option.ClientOption) (c *Client, err error) {
372+
return newClientWithConfig(ctx, database, config, nil, opts...)
373+
}
374+
375+
func newClientWithConfig(ctx context.Context, database string, config ClientConfig, gme *grpcgcp.GCPMultiEndpoint, opts ...option.ClientOption) (c *Client, err error) {
244376
// Validate database path.
245377
if err := validDatabaseName(database); err != nil {
246378
return nil, err
@@ -265,16 +397,25 @@ func NewClientWithConfig(ctx context.Context, database string, config ClientConf
265397
if config.NumChannels == 0 {
266398
config.NumChannels = numChannels
267399
}
268-
// gRPC options.
269-
allOpts := allClientOpts(config.NumChannels, config.Compression, opts...)
270-
pool, err := gtransport.DialPool(ctx, allOpts...)
271-
if err != nil {
272-
return nil, err
273-
}
274400

275-
if hasNumChannelsConfig && pool.Num() != config.NumChannels {
276-
pool.Close()
277-
return nil, spannerErrorf(codes.InvalidArgument, "Connection pool mismatch: NumChannels=%v, WithGRPCConnectionPool=%v. Only set one of these options, or set both to the same value.", config.NumChannels, pool.Num())
401+
var pool gtransport.ConnPool
402+
403+
if gme != nil {
404+
// Use GCPMultiEndpoint if provided.
405+
pool = &gmeWrapper{gme}
406+
} else {
407+
// Create gtransport ConnPool as usual if MultiEndpoint is not used.
408+
// gRPC options.
409+
allOpts := allClientOpts(config.NumChannels, config.Compression, opts...)
410+
pool, err = gtransport.DialPool(ctx, allOpts...)
411+
if err != nil {
412+
return nil, err
413+
}
414+
415+
if hasNumChannelsConfig && pool.Num() != config.NumChannels {
416+
pool.Close()
417+
return nil, spannerErrorf(codes.InvalidArgument, "Connection pool mismatch: NumChannels=%v, WithGRPCConnectionPool=%v. Only set one of these options, or set both to the same value.", config.NumChannels, pool.Num())
418+
}
278419
}
279420

280421
// TODO(loite): Remove as the original map cannot be changed by the user
@@ -343,6 +484,48 @@ func NewClientWithConfig(ctx context.Context, database string, config ClientConf
343484
return c, nil
344485
}
345486

487+
// NewMultiEndpointClient is the same as NewMultiEndpointClientWithConfig with
488+
// the default client configuration.
489+
//
490+
// A valid database name has the
491+
// form projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID.
492+
func NewMultiEndpointClient(ctx context.Context, database string, gmeCfg *grpcgcp.GCPMultiEndpointOptions, opts ...option.ClientOption) (*Client, *grpcgcp.GCPMultiEndpoint, error) {
493+
return NewMultiEndpointClientWithConfig(ctx, database, ClientConfig{SessionPoolConfig: DefaultSessionPoolConfig, DisableRouteToLeader: false}, gmeCfg, opts...)
494+
}
495+
496+
// NewMultiEndpointClientWithConfig creates a client to a database using GCPMultiEndpoint.
497+
//
498+
// The purposes of GCPMultiEndpoint are:
499+
//
500+
// - Fallback to an alternative endpoint (host:port) when the original
501+
// endpoint is completely unavailable.
502+
// - Be able to route a Cloud Spanner call to a specific group of endpoints.
503+
// - Be able to reconfigure endpoints in runtime.
504+
//
505+
// The GRPCgcpConfig and DialFunc in the GCPMultiEndpointOptions are optional
506+
// and will be configured automatically.
507+
//
508+
// For GCPMultiEndpoint the number of channels is configured via MaxSize of the
509+
// ChannelPool config in the GRPCgcpConfig.
510+
//
511+
// The GCPMultiEndpoint returned can be used to update the endpoints in runtime.
512+
//
513+
// A valid database name has the
514+
// form projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID.
515+
func NewMultiEndpointClientWithConfig(ctx context.Context, database string, config ClientConfig, gmeCfg *grpcgcp.GCPMultiEndpointOptions, opts ...option.ClientOption) (c *Client, gme *grpcgcp.GCPMultiEndpoint, err error) {
516+
gme, err = createGCPMultiEndpoint(gmeCfg, config, opts...)
517+
if err != nil {
518+
return nil, nil, err
519+
}
520+
// Align number of channels.
521+
config.NumChannels = int(gme.GCPConfig().GetChannelPool().GetMaxSize())
522+
c, err = newClientWithConfig(ctx, database, config, gme, opts...)
523+
if err != nil {
524+
return nil, nil, err
525+
}
526+
return
527+
}
528+
346529
// Combines the default options from the generated client, the default options
347530
// of the hand-written client and the user options to one list of options.
348531
// Precedence: userOpts > clientDefaultOpts > generatedDefaultOpts

0 commit comments

Comments
 (0)