vendor: update all dependencies to latest versions
This commit is contained in:
+24
-32
@@ -35,11 +35,6 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
databaseAdminInstancePathTemplate = gax.MustCompilePathTemplate("projects/{project}/instances/{instance}")
|
||||
databaseAdminDatabasePathTemplate = gax.MustCompilePathTemplate("projects/{project}/instances/{instance}/databases/{database}")
|
||||
)
|
||||
|
||||
// DatabaseAdminCallOptions contains the retry settings for each method of DatabaseAdminClient.
|
||||
type DatabaseAdminCallOptions struct {
|
||||
ListDatabases []gax.CallOption
|
||||
@@ -126,7 +121,7 @@ func NewDatabaseAdminClient(ctx context.Context, opts ...option.ClientOption) (*
|
||||
|
||||
databaseAdminClient: databasepb.NewDatabaseAdminClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
c.setGoogleClientInfo()
|
||||
|
||||
c.LROClient, err = lroauto.NewOperationsClient(ctx, option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
@@ -152,10 +147,10 @@ func (c *DatabaseAdminClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// setGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *DatabaseAdminClient) SetGoogleClientInfo(keyval ...string) {
|
||||
func (c *DatabaseAdminClient) setGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogHeader = []string{gax.XGoogHeader(kv...)}
|
||||
@@ -163,27 +158,24 @@ func (c *DatabaseAdminClient) SetGoogleClientInfo(keyval ...string) {
|
||||
|
||||
// DatabaseAdminInstancePath returns the path for the instance resource.
|
||||
func DatabaseAdminInstancePath(project, instance string) string {
|
||||
path, err := databaseAdminInstancePathTemplate.Render(map[string]string{
|
||||
"project": project,
|
||||
"instance": instance,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return path
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instances/" +
|
||||
instance +
|
||||
""
|
||||
}
|
||||
|
||||
// DatabaseAdminDatabasePath returns the path for the database resource.
|
||||
func DatabaseAdminDatabasePath(project, instance, database string) string {
|
||||
path, err := databaseAdminDatabasePathTemplate.Render(map[string]string{
|
||||
"project": project,
|
||||
"instance": instance,
|
||||
"database": database,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return path
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instances/" +
|
||||
instance +
|
||||
"/databases/" +
|
||||
database +
|
||||
""
|
||||
}
|
||||
|
||||
// ListDatabases lists Cloud Spanner databases.
|
||||
@@ -223,7 +215,7 @@ func (c *DatabaseAdminClient) ListDatabases(ctx context.Context, req *databasepb
|
||||
|
||||
// CreateDatabase creates a new Cloud Spanner database and starts to prepare it for serving.
|
||||
// The returned [long-running operation][google.longrunning.Operation] will
|
||||
// have a name of the format `<database_name>/operations/<operation_id>` and
|
||||
// have a name of the format <database_name>/operations/<operation_id> and
|
||||
// can be used to track preparation of the database. The
|
||||
// [metadata][google.longrunning.Operation.metadata] field type is
|
||||
// [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. The
|
||||
@@ -265,7 +257,7 @@ func (c *DatabaseAdminClient) GetDatabase(ctx context.Context, req *databasepb.G
|
||||
// UpdateDatabaseDdl updates the schema of a Cloud Spanner database by
|
||||
// creating/altering/dropping tables, columns, indexes, etc. The returned
|
||||
// [long-running operation][google.longrunning.Operation] will have a name of
|
||||
// the format `<database_name>/operations/<operation_id>` and can be used to
|
||||
// the format <database_name>/operations/<operation_id> and can be used to
|
||||
// track execution of the schema change(s). The
|
||||
// [metadata][google.longrunning.Operation.metadata] field type is
|
||||
// [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The operation has no response.
|
||||
@@ -319,7 +311,7 @@ func (c *DatabaseAdminClient) GetDatabaseDdl(ctx context.Context, req *databasep
|
||||
// SetIamPolicy sets the access control policy on a database resource. Replaces any
|
||||
// existing policy.
|
||||
//
|
||||
// Authorization requires `spanner.databases.setIamPolicy` permission on
|
||||
// Authorization requires spanner.databases.setIamPolicy permission on
|
||||
// [resource][google.iam.v1.SetIamPolicyRequest.resource].
|
||||
func (c *DatabaseAdminClient) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
@@ -339,7 +331,7 @@ func (c *DatabaseAdminClient) SetIamPolicy(ctx context.Context, req *iampb.SetIa
|
||||
// GetIamPolicy gets the access control policy for a database resource. Returns an empty
|
||||
// policy if a database exists but does not have a policy set.
|
||||
//
|
||||
// Authorization requires `spanner.databases.getIamPolicy` permission on
|
||||
// Authorization requires spanner.databases.getIamPolicy permission on
|
||||
// [resource][google.iam.v1.GetIamPolicyRequest.resource].
|
||||
func (c *DatabaseAdminClient) GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
@@ -359,7 +351,7 @@ func (c *DatabaseAdminClient) GetIamPolicy(ctx context.Context, req *iampb.GetIa
|
||||
// TestIamPermissions returns permissions that the caller has on the specified database resource.
|
||||
//
|
||||
// Attempting this RPC on a non-existent Cloud Spanner database will result in
|
||||
// a NOT_FOUND error if the user has `spanner.databases.list` permission on
|
||||
// a NOT_FOUND error if the user has spanner.databases.list permission on
|
||||
// the containing Cloud Spanner instance. Otherwise returns an empty set of
|
||||
// permissions.
|
||||
func (c *DatabaseAdminClient) TestIamPermissions(ctx context.Context, req *iampb.TestIamPermissionsRequest, opts ...gax.CallOption) (*iampb.TestIamPermissionsResponse, error) {
|
||||
@@ -437,7 +429,7 @@ func (c *DatabaseAdminClient) CreateDatabaseOperation(name string) *CreateDataba
|
||||
// See documentation of Poll for error-handling information.
|
||||
func (op *CreateDatabaseOperation) Wait(ctx context.Context, opts ...gax.CallOption) (*databasepb.Database, error) {
|
||||
var resp databasepb.Database
|
||||
if err := op.lro.Wait(ctx, &resp, opts...); err != nil {
|
||||
if err := op.lro.WaitWithInterval(ctx, &resp, 45000*time.Millisecond, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
@@ -505,7 +497,7 @@ func (c *DatabaseAdminClient) UpdateDatabaseDdlOperation(name string) *UpdateDat
|
||||
//
|
||||
// See documentation of Poll for error-handling information.
|
||||
func (op *UpdateDatabaseDdlOperation) Wait(ctx context.Context, opts ...gax.CallOption) error {
|
||||
return op.lro.Wait(ctx, nil, opts...)
|
||||
return op.lro.WaitWithInterval(ctx, nil, 45000*time.Millisecond, opts...)
|
||||
}
|
||||
|
||||
// Poll fetches the latest state of the long-running operation.
|
||||
|
||||
+1
-2
@@ -31,8 +31,7 @@ func insertXGoog(ctx context.Context, val []string) context.Context {
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the authentication scopes required
|
||||
// by this package.
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
+1
-2
@@ -31,8 +31,7 @@ func insertXGoog(ctx context.Context, val []string) context.Context {
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the authentication scopes required
|
||||
// by this package.
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
+69
-73
@@ -35,12 +35,6 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceAdminProjectPathTemplate = gax.MustCompilePathTemplate("projects/{project}")
|
||||
instanceAdminInstanceConfigPathTemplate = gax.MustCompilePathTemplate("projects/{project}/instanceConfigs/{instance_config}")
|
||||
instanceAdminInstancePathTemplate = gax.MustCompilePathTemplate("projects/{project}/instances/{instance}")
|
||||
)
|
||||
|
||||
// InstanceAdminCallOptions contains the retry settings for each method of InstanceAdminClient.
|
||||
type InstanceAdminCallOptions struct {
|
||||
ListInstanceConfigs []gax.CallOption
|
||||
@@ -145,7 +139,7 @@ func NewInstanceAdminClient(ctx context.Context, opts ...option.ClientOption) (*
|
||||
|
||||
instanceAdminClient: instancepb.NewInstanceAdminClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
c.setGoogleClientInfo()
|
||||
|
||||
c.LROClient, err = lroauto.NewOperationsClient(ctx, option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
@@ -171,10 +165,10 @@ func (c *InstanceAdminClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// setGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *InstanceAdminClient) SetGoogleClientInfo(keyval ...string) {
|
||||
func (c *InstanceAdminClient) setGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogHeader = []string{gax.XGoogHeader(kv...)}
|
||||
@@ -182,37 +176,30 @@ func (c *InstanceAdminClient) SetGoogleClientInfo(keyval ...string) {
|
||||
|
||||
// InstanceAdminProjectPath returns the path for the project resource.
|
||||
func InstanceAdminProjectPath(project string) string {
|
||||
path, err := instanceAdminProjectPathTemplate.Render(map[string]string{
|
||||
"project": project,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return path
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
""
|
||||
}
|
||||
|
||||
// InstanceAdminInstanceConfigPath returns the path for the instance config resource.
|
||||
func InstanceAdminInstanceConfigPath(project, instanceConfig string) string {
|
||||
path, err := instanceAdminInstanceConfigPathTemplate.Render(map[string]string{
|
||||
"project": project,
|
||||
"instance_config": instanceConfig,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return path
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instanceConfigs/" +
|
||||
instanceConfig +
|
||||
""
|
||||
}
|
||||
|
||||
// InstanceAdminInstancePath returns the path for the instance resource.
|
||||
func InstanceAdminInstancePath(project, instance string) string {
|
||||
path, err := instanceAdminInstancePathTemplate.Render(map[string]string{
|
||||
"project": project,
|
||||
"instance": instance,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return path
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instances/" +
|
||||
instance +
|
||||
""
|
||||
}
|
||||
|
||||
// ListInstanceConfigs lists the supported instance configurations for a given project.
|
||||
@@ -321,31 +308,36 @@ func (c *InstanceAdminClient) GetInstance(ctx context.Context, req *instancepb.G
|
||||
// returned [long-running operation][google.longrunning.Operation]
|
||||
// can be used to track the progress of preparing the new
|
||||
// instance. The instance name is assigned by the caller. If the
|
||||
// named instance already exists, `CreateInstance` returns
|
||||
// `ALREADY_EXISTS`.
|
||||
// named instance already exists, CreateInstance returns
|
||||
// ALREADY_EXISTS.
|
||||
//
|
||||
// Immediately upon completion of this request:
|
||||
//
|
||||
// * The instance is readable via the API, with all requested attributes
|
||||
// but no allocated resources. Its state is `CREATING`.
|
||||
// The instance is readable via the API, with all requested attributes
|
||||
// but no allocated resources. Its state is CREATING.
|
||||
//
|
||||
// Until completion of the returned operation:
|
||||
//
|
||||
// * Cancelling the operation renders the instance immediately unreadable
|
||||
// via the API.
|
||||
// * The instance can be deleted.
|
||||
// * All other attempts to modify the instance are rejected.
|
||||
// Cancelling the operation renders the instance immediately unreadable
|
||||
// via the API.
|
||||
//
|
||||
// The instance can be deleted.
|
||||
//
|
||||
// All other attempts to modify the instance are rejected.
|
||||
//
|
||||
// Upon completion of the returned operation:
|
||||
//
|
||||
// * Billing for all successfully-allocated resources begins (some types
|
||||
// may have lower than the requested levels).
|
||||
// * Databases can be created in the instance.
|
||||
// * The instance's allocated resource levels are readable via the API.
|
||||
// * The instance's state becomes `READY`.
|
||||
// Billing for all successfully-allocated resources begins (some types
|
||||
// may have lower than the requested levels).
|
||||
//
|
||||
// Databases can be created in the instance.
|
||||
//
|
||||
// The instance's allocated resource levels are readable via the API.
|
||||
//
|
||||
// The instance's state becomes READY.
|
||||
//
|
||||
// The returned [long-running operation][google.longrunning.Operation] will
|
||||
// have a name of the format `<instance_name>/operations/<operation_id>` and
|
||||
// have a name of the format <instance_name>/operations/<operation_id> and
|
||||
// can be used to track creation of the instance. The
|
||||
// [metadata][google.longrunning.Operation.metadata] field type is
|
||||
// [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata].
|
||||
@@ -372,41 +364,45 @@ func (c *InstanceAdminClient) CreateInstance(ctx context.Context, req *instancep
|
||||
// as requested. The returned [long-running
|
||||
// operation][google.longrunning.Operation] can be used to track the
|
||||
// progress of updating the instance. If the named instance does not
|
||||
// exist, returns `NOT_FOUND`.
|
||||
// exist, returns NOT_FOUND.
|
||||
//
|
||||
// Immediately upon completion of this request:
|
||||
//
|
||||
// * For resource types for which a decrease in the instance's allocation
|
||||
// has been requested, billing is based on the newly-requested level.
|
||||
// For resource types for which a decrease in the instance's allocation
|
||||
// has been requested, billing is based on the newly-requested level.
|
||||
//
|
||||
// Until completion of the returned operation:
|
||||
//
|
||||
// * Cancelling the operation sets its metadata's
|
||||
// [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins
|
||||
// restoring resources to their pre-request values. The operation
|
||||
// is guaranteed to succeed at undoing all resource changes,
|
||||
// after which point it terminates with a `CANCELLED` status.
|
||||
// * All other attempts to modify the instance are rejected.
|
||||
// * Reading the instance via the API continues to give the pre-request
|
||||
// resource levels.
|
||||
// Cancelling the operation sets its metadata's
|
||||
// [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins
|
||||
// restoring resources to their pre-request values. The operation
|
||||
// is guaranteed to succeed at undoing all resource changes,
|
||||
// after which point it terminates with a CANCELLED status.
|
||||
//
|
||||
// All other attempts to modify the instance are rejected.
|
||||
//
|
||||
// Reading the instance via the API continues to give the pre-request
|
||||
// resource levels.
|
||||
//
|
||||
// Upon completion of the returned operation:
|
||||
//
|
||||
// * Billing begins for all successfully-allocated resources (some types
|
||||
// may have lower than the requested levels).
|
||||
// * All newly-reserved resources are available for serving the instance's
|
||||
// tables.
|
||||
// * The instance's new resource levels are readable via the API.
|
||||
// Billing begins for all successfully-allocated resources (some types
|
||||
// may have lower than the requested levels).
|
||||
//
|
||||
// All newly-reserved resources are available for serving the instance's
|
||||
// tables.
|
||||
//
|
||||
// The instance's new resource levels are readable via the API.
|
||||
//
|
||||
// The returned [long-running operation][google.longrunning.Operation] will
|
||||
// have a name of the format `<instance_name>/operations/<operation_id>` and
|
||||
// have a name of the format <instance_name>/operations/<operation_id> and
|
||||
// can be used to track the instance modification. The
|
||||
// [metadata][google.longrunning.Operation.metadata] field type is
|
||||
// [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata].
|
||||
// The [response][google.longrunning.Operation.response] field type is
|
||||
// [Instance][google.spanner.admin.instance.v1.Instance], if successful.
|
||||
//
|
||||
// Authorization requires `spanner.instances.update` permission on
|
||||
// Authorization requires spanner.instances.update permission on
|
||||
// resource [name][google.spanner.admin.instance.v1.Instance.name].
|
||||
func (c *InstanceAdminClient) UpdateInstance(ctx context.Context, req *instancepb.UpdateInstanceRequest, opts ...gax.CallOption) (*UpdateInstanceOperation, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
@@ -429,13 +425,13 @@ func (c *InstanceAdminClient) UpdateInstance(ctx context.Context, req *instancep
|
||||
//
|
||||
// Immediately upon completion of the request:
|
||||
//
|
||||
// * Billing ceases for all of the instance's reserved resources.
|
||||
// Billing ceases for all of the instance's reserved resources.
|
||||
//
|
||||
// Soon afterward:
|
||||
//
|
||||
// * The instance and *all of its databases* immediately and
|
||||
// irrevocably disappear from the API. All data in the databases
|
||||
// is permanently deleted.
|
||||
// The instance and all of its databases immediately and
|
||||
// irrevocably disappear from the API. All data in the databases
|
||||
// is permanently deleted.
|
||||
func (c *InstanceAdminClient) DeleteInstance(ctx context.Context, req *instancepb.DeleteInstanceRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.DeleteInstance[0:len(c.CallOptions.DeleteInstance):len(c.CallOptions.DeleteInstance)], opts...)
|
||||
@@ -450,7 +446,7 @@ func (c *InstanceAdminClient) DeleteInstance(ctx context.Context, req *instancep
|
||||
// SetIamPolicy sets the access control policy on an instance resource. Replaces any
|
||||
// existing policy.
|
||||
//
|
||||
// Authorization requires `spanner.instances.setIamPolicy` on
|
||||
// Authorization requires spanner.instances.setIamPolicy on
|
||||
// [resource][google.iam.v1.SetIamPolicyRequest.resource].
|
||||
func (c *InstanceAdminClient) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
@@ -470,7 +466,7 @@ func (c *InstanceAdminClient) SetIamPolicy(ctx context.Context, req *iampb.SetIa
|
||||
// GetIamPolicy gets the access control policy for an instance resource. Returns an empty
|
||||
// policy if an instance exists but does not have a policy set.
|
||||
//
|
||||
// Authorization requires `spanner.instances.getIamPolicy` on
|
||||
// Authorization requires spanner.instances.getIamPolicy on
|
||||
// [resource][google.iam.v1.GetIamPolicyRequest.resource].
|
||||
func (c *InstanceAdminClient) GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
@@ -490,7 +486,7 @@ func (c *InstanceAdminClient) GetIamPolicy(ctx context.Context, req *iampb.GetIa
|
||||
// TestIamPermissions returns permissions that the caller has on the specified instance resource.
|
||||
//
|
||||
// Attempting this RPC on a non-existent Cloud Spanner instance resource will
|
||||
// result in a NOT_FOUND error if the user has `spanner.instances.list`
|
||||
// result in a NOT_FOUND error if the user has spanner.instances.list
|
||||
// permission on the containing Google Cloud Project. Otherwise returns an
|
||||
// empty set of permissions.
|
||||
func (c *InstanceAdminClient) TestIamPermissions(ctx context.Context, req *iampb.TestIamPermissionsRequest, opts ...gax.CallOption) (*iampb.TestIamPermissionsResponse, error) {
|
||||
@@ -610,7 +606,7 @@ func (c *InstanceAdminClient) CreateInstanceOperation(name string) *CreateInstan
|
||||
// See documentation of Poll for error-handling information.
|
||||
func (op *CreateInstanceOperation) Wait(ctx context.Context, opts ...gax.CallOption) (*instancepb.Instance, error) {
|
||||
var resp instancepb.Instance
|
||||
if err := op.lro.Wait(ctx, &resp, opts...); err != nil {
|
||||
if err := op.lro.WaitWithInterval(ctx, &resp, 45000*time.Millisecond, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
@@ -679,7 +675,7 @@ func (c *InstanceAdminClient) UpdateInstanceOperation(name string) *UpdateInstan
|
||||
// See documentation of Poll for error-handling information.
|
||||
func (op *UpdateInstanceOperation) Wait(ctx context.Context, opts ...gax.CallOption) (*instancepb.Instance, error) {
|
||||
var resp instancepb.Instance
|
||||
if err := op.lro.Wait(ctx, &resp, opts...); err != nil {
|
||||
if err := op.lro.WaitWithInterval(ctx, &resp, 45000*time.Millisecond, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2017, Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
// Package spanner is an experimental, auto-generated package for the
|
||||
// Cloud Spanner API.
|
||||
//
|
||||
// Cloud Spanner is a managed, mission-critical, globally consistent and
|
||||
// scalable relational database service.
|
||||
//
|
||||
// Use the client at cloud.google.com/go/spanner in preference to this.
|
||||
package spanner // import "cloud.google.com/go/spanner/apiv1"
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func insertXGoog(ctx context.Context, val []string) context.Context {
|
||||
md, _ := metadata.FromOutgoingContext(ctx)
|
||||
md = md.Copy()
|
||||
md["x-goog-api-client"] = val
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/spanner.data",
|
||||
}
|
||||
}
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
// Copyright 2017, Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package spanner
|
||||
|
||||
import (
|
||||
emptypb "github.com/golang/protobuf/ptypes/empty"
|
||||
spannerpb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
)
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
status "google.golang.org/genproto/googleapis/rpc/status"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ = io.EOF
|
||||
var _ = ptypes.MarshalAny
|
||||
var _ status.Status
|
||||
|
||||
type mockSpannerServer struct {
|
||||
// Embed for forward compatibility.
|
||||
// Tests will keep working if more methods are added
|
||||
// in the future.
|
||||
spannerpb.SpannerServer
|
||||
|
||||
reqs []proto.Message
|
||||
|
||||
// If set, all calls return this error.
|
||||
err error
|
||||
|
||||
// responses to return if err == nil
|
||||
resps []proto.Message
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) CreateSession(ctx context.Context, req *spannerpb.CreateSessionRequest) (*spannerpb.Session, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.Session), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) GetSession(ctx context.Context, req *spannerpb.GetSessionRequest) (*spannerpb.Session, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.Session), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) DeleteSession(ctx context.Context, req *spannerpb.DeleteSessionRequest) (*emptypb.Empty, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*emptypb.Empty), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) ExecuteSql(ctx context.Context, req *spannerpb.ExecuteSqlRequest) (*spannerpb.ResultSet, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.ResultSet), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) ExecuteStreamingSql(req *spannerpb.ExecuteSqlRequest, stream spannerpb.Spanner_ExecuteStreamingSqlServer) error {
|
||||
md, _ := metadata.FromIncomingContext(stream.Context())
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return s.err
|
||||
}
|
||||
for _, v := range s.resps {
|
||||
if err := stream.Send(v.(*spannerpb.PartialResultSet)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) Read(ctx context.Context, req *spannerpb.ReadRequest) (*spannerpb.ResultSet, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.ResultSet), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) StreamingRead(req *spannerpb.ReadRequest, stream spannerpb.Spanner_StreamingReadServer) error {
|
||||
md, _ := metadata.FromIncomingContext(stream.Context())
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return s.err
|
||||
}
|
||||
for _, v := range s.resps {
|
||||
if err := stream.Send(v.(*spannerpb.PartialResultSet)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) BeginTransaction(ctx context.Context, req *spannerpb.BeginTransactionRequest) (*spannerpb.Transaction, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.Transaction), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) Commit(ctx context.Context, req *spannerpb.CommitRequest) (*spannerpb.CommitResponse, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*spannerpb.CommitResponse), nil
|
||||
}
|
||||
|
||||
func (s *mockSpannerServer) Rollback(ctx context.Context, req *spannerpb.RollbackRequest) (*emptypb.Empty, error) {
|
||||
md, _ := metadata.FromIncomingContext(ctx)
|
||||
if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
|
||||
return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
|
||||
}
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.resps[0].(*emptypb.Empty), nil
|
||||
}
|
||||
|
||||
// clientOpt is the option tests should use to connect to the test server.
|
||||
// It is initialized by TestMain.
|
||||
var clientOpt option.ClientOption
|
||||
|
||||
var (
|
||||
mockSpanner mockSpannerServer
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
serv := grpc.NewServer()
|
||||
spannerpb.RegisterSpannerServer(serv, &mockSpanner)
|
||||
|
||||
lis, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go serv.Serve(lis)
|
||||
|
||||
conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clientOpt = option.WithGRPCConn(conn)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestSpannerCreateSession(t *testing.T) {
|
||||
var name string = "name3373707"
|
||||
var expectedResponse = &spannerpb.Session{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedDatabase string = DatabasePath("[PROJECT]", "[INSTANCE]", "[DATABASE]")
|
||||
var request = &spannerpb.CreateSessionRequest{
|
||||
Database: formattedDatabase,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.CreateSession(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerCreateSessionError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedDatabase string = DatabasePath("[PROJECT]", "[INSTANCE]", "[DATABASE]")
|
||||
var request = &spannerpb.CreateSessionRequest{
|
||||
Database: formattedDatabase,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.CreateSession(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerGetSession(t *testing.T) {
|
||||
var name2 string = "name2-1052831874"
|
||||
var expectedResponse = &spannerpb.Session{
|
||||
Name: name2,
|
||||
}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedName string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var request = &spannerpb.GetSessionRequest{
|
||||
Name: formattedName,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.GetSession(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerGetSessionError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedName string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var request = &spannerpb.GetSessionRequest{
|
||||
Name: formattedName,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.GetSession(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerDeleteSession(t *testing.T) {
|
||||
var expectedResponse *emptypb.Empty = &emptypb.Empty{}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedName string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var request = &spannerpb.DeleteSessionRequest{
|
||||
Name: formattedName,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.DeleteSession(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSpannerDeleteSessionError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedName string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var request = &spannerpb.DeleteSessionRequest{
|
||||
Name: formattedName,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.DeleteSession(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
}
|
||||
func TestSpannerExecuteSql(t *testing.T) {
|
||||
var expectedResponse *spannerpb.ResultSet = &spannerpb.ResultSet{}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var sql string = "sql114126"
|
||||
var request = &spannerpb.ExecuteSqlRequest{
|
||||
Session: formattedSession,
|
||||
Sql: sql,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.ExecuteSql(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerExecuteSqlError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var sql string = "sql114126"
|
||||
var request = &spannerpb.ExecuteSqlRequest{
|
||||
Session: formattedSession,
|
||||
Sql: sql,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.ExecuteSql(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerExecuteStreamingSql(t *testing.T) {
|
||||
var chunkedValue bool = true
|
||||
var resumeToken []byte = []byte("103")
|
||||
var expectedResponse = &spannerpb.PartialResultSet{
|
||||
ChunkedValue: chunkedValue,
|
||||
ResumeToken: resumeToken,
|
||||
}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var sql string = "sql114126"
|
||||
var request = &spannerpb.ExecuteSqlRequest{
|
||||
Session: formattedSession,
|
||||
Sql: sql,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stream, err := c.ExecuteStreamingSql(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerExecuteStreamingSqlError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var sql string = "sql114126"
|
||||
var request = &spannerpb.ExecuteSqlRequest{
|
||||
Session: formattedSession,
|
||||
Sql: sql,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stream, err := c.ExecuteStreamingSql(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerRead(t *testing.T) {
|
||||
var expectedResponse *spannerpb.ResultSet = &spannerpb.ResultSet{}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var table string = "table110115790"
|
||||
var columns []string = nil
|
||||
var keySet *spannerpb.KeySet = &spannerpb.KeySet{}
|
||||
var request = &spannerpb.ReadRequest{
|
||||
Session: formattedSession,
|
||||
Table: table,
|
||||
Columns: columns,
|
||||
KeySet: keySet,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.Read(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerReadError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var table string = "table110115790"
|
||||
var columns []string = nil
|
||||
var keySet *spannerpb.KeySet = &spannerpb.KeySet{}
|
||||
var request = &spannerpb.ReadRequest{
|
||||
Session: formattedSession,
|
||||
Table: table,
|
||||
Columns: columns,
|
||||
KeySet: keySet,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.Read(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerStreamingRead(t *testing.T) {
|
||||
var chunkedValue bool = true
|
||||
var resumeToken []byte = []byte("103")
|
||||
var expectedResponse = &spannerpb.PartialResultSet{
|
||||
ChunkedValue: chunkedValue,
|
||||
ResumeToken: resumeToken,
|
||||
}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var table string = "table110115790"
|
||||
var columns []string = nil
|
||||
var keySet *spannerpb.KeySet = &spannerpb.KeySet{}
|
||||
var request = &spannerpb.ReadRequest{
|
||||
Session: formattedSession,
|
||||
Table: table,
|
||||
Columns: columns,
|
||||
KeySet: keySet,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stream, err := c.StreamingRead(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerStreamingReadError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var table string = "table110115790"
|
||||
var columns []string = nil
|
||||
var keySet *spannerpb.KeySet = &spannerpb.KeySet{}
|
||||
var request = &spannerpb.ReadRequest{
|
||||
Session: formattedSession,
|
||||
Table: table,
|
||||
Columns: columns,
|
||||
KeySet: keySet,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stream, err := c.StreamingRead(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := stream.Recv()
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerBeginTransaction(t *testing.T) {
|
||||
var id []byte = []byte("27")
|
||||
var expectedResponse = &spannerpb.Transaction{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var options *spannerpb.TransactionOptions = &spannerpb.TransactionOptions{}
|
||||
var request = &spannerpb.BeginTransactionRequest{
|
||||
Session: formattedSession,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.BeginTransaction(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerBeginTransactionError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var options *spannerpb.TransactionOptions = &spannerpb.TransactionOptions{}
|
||||
var request = &spannerpb.BeginTransactionRequest{
|
||||
Session: formattedSession,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.BeginTransaction(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerCommit(t *testing.T) {
|
||||
var expectedResponse *spannerpb.CommitResponse = &spannerpb.CommitResponse{}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var mutations []*spannerpb.Mutation = nil
|
||||
var request = &spannerpb.CommitRequest{
|
||||
Session: formattedSession,
|
||||
Mutations: mutations,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.Commit(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if want, got := expectedResponse, resp; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong response %q, want %q)", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpannerCommitError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var mutations []*spannerpb.Mutation = nil
|
||||
var request = &spannerpb.CommitRequest{
|
||||
Session: formattedSession,
|
||||
Mutations: mutations,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := c.Commit(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
_ = resp
|
||||
}
|
||||
func TestSpannerRollback(t *testing.T) {
|
||||
var expectedResponse *emptypb.Empty = &emptypb.Empty{}
|
||||
|
||||
mockSpanner.err = nil
|
||||
mockSpanner.reqs = nil
|
||||
|
||||
mockSpanner.resps = append(mockSpanner.resps[:0], expectedResponse)
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var transactionId []byte = []byte("28")
|
||||
var request = &spannerpb.RollbackRequest{
|
||||
Session: formattedSession,
|
||||
TransactionId: transactionId,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Rollback(context.Background(), request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := request, mockSpanner.reqs[0]; !proto.Equal(want, got) {
|
||||
t.Errorf("wrong request %q, want %q", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSpannerRollbackError(t *testing.T) {
|
||||
errCode := codes.PermissionDenied
|
||||
mockSpanner.err = gstatus.Error(errCode, "test error")
|
||||
|
||||
var formattedSession string = SessionPath("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]")
|
||||
var transactionId []byte = []byte("28")
|
||||
var request = &spannerpb.RollbackRequest{
|
||||
Session: formattedSession,
|
||||
TransactionId: transactionId,
|
||||
}
|
||||
|
||||
c, err := NewClient(context.Background(), clientOpt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Rollback(context.Background(), request)
|
||||
|
||||
if st, ok := gstatus.FromError(err); !ok {
|
||||
t.Errorf("got error %v, expected grpc error", err)
|
||||
} else if c := st.Code(); c != errCode {
|
||||
t.Errorf("got error code %q, want %q", c, errCode)
|
||||
}
|
||||
}
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
// Copyright 2017, Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/version"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
spannerpb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// CallOptions contains the retry settings for each method of Client.
|
||||
type CallOptions struct {
|
||||
CreateSession []gax.CallOption
|
||||
GetSession []gax.CallOption
|
||||
DeleteSession []gax.CallOption
|
||||
ExecuteSql []gax.CallOption
|
||||
ExecuteStreamingSql []gax.CallOption
|
||||
Read []gax.CallOption
|
||||
StreamingRead []gax.CallOption
|
||||
BeginTransaction []gax.CallOption
|
||||
Commit []gax.CallOption
|
||||
Rollback []gax.CallOption
|
||||
}
|
||||
|
||||
func defaultClientOptions() []option.ClientOption {
|
||||
return []option.ClientOption{
|
||||
option.WithEndpoint("spanner.googleapis.com:443"),
|
||||
option.WithScopes(DefaultAuthScopes()...),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCallOptions() *CallOptions {
|
||||
retry := map[[2]string][]gax.CallOption{
|
||||
{"default", "idempotent"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 1000 * time.Millisecond,
|
||||
Max: 32000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
{"long_running", "long_running"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 1000 * time.Millisecond,
|
||||
Max: 32000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
return &CallOptions{
|
||||
CreateSession: retry[[2]string{"default", "idempotent"}],
|
||||
GetSession: retry[[2]string{"default", "idempotent"}],
|
||||
DeleteSession: retry[[2]string{"default", "idempotent"}],
|
||||
ExecuteSql: retry[[2]string{"default", "idempotent"}],
|
||||
ExecuteStreamingSql: retry[[2]string{"default", "non_idempotent"}],
|
||||
Read: retry[[2]string{"default", "idempotent"}],
|
||||
StreamingRead: retry[[2]string{"default", "non_idempotent"}],
|
||||
BeginTransaction: retry[[2]string{"default", "idempotent"}],
|
||||
Commit: retry[[2]string{"long_running", "long_running"}],
|
||||
Rollback: retry[[2]string{"default", "idempotent"}],
|
||||
}
|
||||
}
|
||||
|
||||
// Client is a client for interacting with Cloud Spanner API.
|
||||
type Client struct {
|
||||
// The connection to the service.
|
||||
conn *grpc.ClientConn
|
||||
|
||||
// The gRPC API client.
|
||||
client spannerpb.SpannerClient
|
||||
|
||||
// The call options for this service.
|
||||
CallOptions *CallOptions
|
||||
|
||||
// The metadata to be sent with each request.
|
||||
xGoogHeader []string
|
||||
}
|
||||
|
||||
// NewClient creates a new spanner client.
|
||||
//
|
||||
// Cloud Spanner API
|
||||
//
|
||||
// The Cloud Spanner API can be used to manage sessions and execute
|
||||
// transactions on data stored in Cloud Spanner databases.
|
||||
func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) {
|
||||
conn, err := transport.DialGRPC(ctx, append(defaultClientOptions(), opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{
|
||||
conn: conn,
|
||||
CallOptions: defaultCallOptions(),
|
||||
|
||||
client: spannerpb.NewSpannerClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Connection returns the client's connection to the API service.
|
||||
func (c *Client) Connection() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// Close closes the connection to the API service. The user should invoke this when
|
||||
// the client is no longer required.
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *Client) SetGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogHeader = []string{gax.XGoogHeader(kv...)}
|
||||
}
|
||||
|
||||
// DatabasePath returns the path for the database resource.
|
||||
func DatabasePath(project, instance, database string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instances/" +
|
||||
instance +
|
||||
"/databases/" +
|
||||
database +
|
||||
""
|
||||
}
|
||||
|
||||
// SessionPath returns the path for the session resource.
|
||||
func SessionPath(project, instance, database, session string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/instances/" +
|
||||
instance +
|
||||
"/databases/" +
|
||||
database +
|
||||
"/sessions/" +
|
||||
session +
|
||||
""
|
||||
}
|
||||
|
||||
// CreateSession creates a new session. A session can be used to perform
|
||||
// transactions that read and/or modify data in a Cloud Spanner database.
|
||||
// Sessions are meant to be reused for many consecutive
|
||||
// transactions.
|
||||
//
|
||||
// Sessions can only execute one transaction at a time. To execute
|
||||
// multiple concurrent read-write/write-only transactions, create
|
||||
// multiple sessions. Note that standalone reads and queries use a
|
||||
// transaction internally, and count toward the one transaction
|
||||
// limit.
|
||||
//
|
||||
// Cloud Spanner limits the number of sessions that can exist at any given
|
||||
// time; thus, it is a good idea to delete idle and/or unneeded sessions.
|
||||
// Aside from explicit deletes, Cloud Spanner can delete sessions for which no
|
||||
// operations are sent for more than an hour. If a session is deleted,
|
||||
// requests to it return NOT_FOUND.
|
||||
//
|
||||
// Idle sessions can be kept alive by sending a trivial SQL query
|
||||
// periodically, e.g., "SELECT 1".
|
||||
func (c *Client) CreateSession(ctx context.Context, req *spannerpb.CreateSessionRequest, opts ...gax.CallOption) (*spannerpb.Session, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.CreateSession[0:len(c.CallOptions.CreateSession):len(c.CallOptions.CreateSession)], opts...)
|
||||
var resp *spannerpb.Session
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.CreateSession(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSession gets a session. Returns NOT_FOUND if the session does not exist.
|
||||
// This is mainly useful for determining whether a session is still
|
||||
// alive.
|
||||
func (c *Client) GetSession(ctx context.Context, req *spannerpb.GetSessionRequest, opts ...gax.CallOption) (*spannerpb.Session, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.GetSession[0:len(c.CallOptions.GetSession):len(c.CallOptions.GetSession)], opts...)
|
||||
var resp *spannerpb.Session
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.GetSession(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteSession ends a session, releasing server resources associated with it.
|
||||
func (c *Client) DeleteSession(ctx context.Context, req *spannerpb.DeleteSessionRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.DeleteSession[0:len(c.CallOptions.DeleteSession):len(c.CallOptions.DeleteSession)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.client.DeleteSession(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecuteSql executes an SQL query, returning all rows in a single reply. This
|
||||
// method cannot be used to return a result set larger than 10 MiB;
|
||||
// if the query yields more data than that, the query fails with
|
||||
// a FAILED_PRECONDITION error.
|
||||
//
|
||||
// Queries inside read-write transactions might return ABORTED. If
|
||||
// this occurs, the application should restart the transaction from
|
||||
// the beginning. See [Transaction][google.spanner.v1.Transaction] for more details.
|
||||
//
|
||||
// Larger result sets can be fetched in streaming fashion by calling
|
||||
// [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] instead.
|
||||
func (c *Client) ExecuteSql(ctx context.Context, req *spannerpb.ExecuteSqlRequest, opts ...gax.CallOption) (*spannerpb.ResultSet, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.ExecuteSql[0:len(c.CallOptions.ExecuteSql):len(c.CallOptions.ExecuteSql)], opts...)
|
||||
var resp *spannerpb.ResultSet
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.ExecuteSql(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ExecuteStreamingSql like [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], except returns the result
|
||||
// set as a stream. Unlike [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], there
|
||||
// is no limit on the size of the returned result set. However, no
|
||||
// individual row in the result set can exceed 100 MiB, and no
|
||||
// column value can exceed 10 MiB.
|
||||
func (c *Client) ExecuteStreamingSql(ctx context.Context, req *spannerpb.ExecuteSqlRequest, opts ...gax.CallOption) (spannerpb.Spanner_ExecuteStreamingSqlClient, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.ExecuteStreamingSql[0:len(c.CallOptions.ExecuteStreamingSql):len(c.CallOptions.ExecuteStreamingSql)], opts...)
|
||||
var resp spannerpb.Spanner_ExecuteStreamingSqlClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.ExecuteStreamingSql(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Read reads rows from the database using key lookups and scans, as a
|
||||
// simple key/value style alternative to
|
||||
// [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. This method cannot be used to
|
||||
// return a result set larger than 10 MiB; if the read matches more
|
||||
// data than that, the read fails with a FAILED_PRECONDITION
|
||||
// error.
|
||||
//
|
||||
// Reads inside read-write transactions might return ABORTED. If
|
||||
// this occurs, the application should restart the transaction from
|
||||
// the beginning. See [Transaction][google.spanner.v1.Transaction] for more details.
|
||||
//
|
||||
// Larger result sets can be yielded in streaming fashion by calling
|
||||
// [StreamingRead][google.spanner.v1.Spanner.StreamingRead] instead.
|
||||
func (c *Client) Read(ctx context.Context, req *spannerpb.ReadRequest, opts ...gax.CallOption) (*spannerpb.ResultSet, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.Read[0:len(c.CallOptions.Read):len(c.CallOptions.Read)], opts...)
|
||||
var resp *spannerpb.ResultSet
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.Read(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// StreamingRead like [Read][google.spanner.v1.Spanner.Read], except returns the result set as a
|
||||
// stream. Unlike [Read][google.spanner.v1.Spanner.Read], there is no limit on the
|
||||
// size of the returned result set. However, no individual row in
|
||||
// the result set can exceed 100 MiB, and no column value can exceed
|
||||
// 10 MiB.
|
||||
func (c *Client) StreamingRead(ctx context.Context, req *spannerpb.ReadRequest, opts ...gax.CallOption) (spannerpb.Spanner_StreamingReadClient, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.StreamingRead[0:len(c.CallOptions.StreamingRead):len(c.CallOptions.StreamingRead)], opts...)
|
||||
var resp spannerpb.Spanner_StreamingReadClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.StreamingRead(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// BeginTransaction begins a new transaction. This step can often be skipped:
|
||||
// [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and
|
||||
// [Commit][google.spanner.v1.Spanner.Commit] can begin a new transaction as a
|
||||
// side-effect.
|
||||
func (c *Client) BeginTransaction(ctx context.Context, req *spannerpb.BeginTransactionRequest, opts ...gax.CallOption) (*spannerpb.Transaction, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.BeginTransaction[0:len(c.CallOptions.BeginTransaction):len(c.CallOptions.BeginTransaction)], opts...)
|
||||
var resp *spannerpb.Transaction
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.BeginTransaction(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Commit commits a transaction. The request includes the mutations to be
|
||||
// applied to rows in the database.
|
||||
//
|
||||
// Commit might return an ABORTED error. This can occur at any time;
|
||||
// commonly, the cause is conflicts with concurrent
|
||||
// transactions. However, it can also happen for a variety of other
|
||||
// reasons. If Commit returns ABORTED, the caller should re-attempt
|
||||
// the transaction from the beginning, re-using the same session.
|
||||
func (c *Client) Commit(ctx context.Context, req *spannerpb.CommitRequest, opts ...gax.CallOption) (*spannerpb.CommitResponse, error) {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.Commit[0:len(c.CallOptions.Commit):len(c.CallOptions.Commit)], opts...)
|
||||
var resp *spannerpb.CommitResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.Commit(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Rollback rolls back a transaction, releasing any locks it holds. It is a good
|
||||
// idea to call this for any transaction that includes one or more
|
||||
// [Read][google.spanner.v1.Spanner.Read] or [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] requests and
|
||||
// ultimately decides not to commit.
|
||||
//
|
||||
// Rollback returns OK if it successfully aborts the transaction, the
|
||||
// transaction was already aborted, or the transaction is not
|
||||
// found. Rollback never returns ABORTED.
|
||||
func (c *Client) Rollback(ctx context.Context, req *spannerpb.RollbackRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertXGoog(ctx, c.xGoogHeader)
|
||||
opts = append(c.CallOptions.Rollback[0:len(c.CallOptions.Rollback):len(c.CallOptions.Rollback)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.client.Rollback(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
// Copyright 2017, Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package spanner_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"cloud.google.com/go/spanner/apiv1"
|
||||
"golang.org/x/net/context"
|
||||
spannerpb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use client.
|
||||
_ = c
|
||||
}
|
||||
|
||||
func ExampleClient_CreateSession() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.CreateSessionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.CreateSession(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_GetSession() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.GetSessionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.GetSession(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_DeleteSession() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.DeleteSessionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.DeleteSession(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_ExecuteSql() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.ExecuteSqlRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.ExecuteSql(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_ExecuteStreamingSql() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.ExecuteSqlRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
stream, err := c.ExecuteStreamingSql(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Read() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.ReadRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Read(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_StreamingRead() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.ReadRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
stream, err := c.StreamingRead(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_BeginTransaction() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.BeginTransactionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.BeginTransaction(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_Commit() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.CommitRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Commit(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_Rollback() {
|
||||
ctx := context.Background()
|
||||
c, err := spanner.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &spannerpb.RollbackRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.Rollback(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
+12
-8
@@ -25,7 +25,7 @@ import (
|
||||
"cloud.google.com/go/internal/version"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
gtransport "google.golang.org/api/transport/grpc"
|
||||
sppb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -38,9 +38,9 @@ const (
|
||||
// resourcePrefixHeader is the name of the metadata header used to indicate
|
||||
// the resource being operated on.
|
||||
resourcePrefixHeader = "google-cloud-resource-prefix"
|
||||
// apiClientHeader is the name of the metadata header used to indicate client
|
||||
// xGoogHeaderKey is the name of the metadata header used to indicate client
|
||||
// information.
|
||||
apiClientHeader = "x-goog-api-client"
|
||||
xGoogHeaderKey = "x-goog-api-client"
|
||||
|
||||
// numChannels is the default value for NumChannels of client
|
||||
numChannels = 4
|
||||
@@ -55,8 +55,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
validDBPattern = regexp.MustCompile("^projects/[^/]+/instances/[^/]+/databases/[^/]+$")
|
||||
clientUserAgent = fmt.Sprintf("gl-go/%s gccl/%s grpc/%s", version.Go(), version.Repo, grpc.Version)
|
||||
validDBPattern = regexp.MustCompile("^projects/[^/]+/instances/[^/]+/databases/[^/]+$")
|
||||
xGoogHeaderVal = fmt.Sprintf("gl-go/%s gccl/%s grpc/%s", version.Go(), version.Repo, grpc.Version)
|
||||
)
|
||||
|
||||
func validDatabaseName(db string) error {
|
||||
@@ -123,9 +123,9 @@ func NewClientWithConfig(ctx context.Context, database string, config ClientConf
|
||||
database: database,
|
||||
md: metadata.Pairs(
|
||||
resourcePrefixHeader, database,
|
||||
apiClientHeader, clientUserAgent),
|
||||
xGoogHeaderKey, xGoogHeaderVal),
|
||||
}
|
||||
allOpts := []option.ClientOption{option.WithEndpoint(prodAddr), option.WithScopes(Scope), option.WithUserAgent(clientUserAgent), option.WithGRPCDialOption(grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20)))}
|
||||
allOpts := []option.ClientOption{option.WithEndpoint(prodAddr), option.WithScopes(Scope), option.WithGRPCDialOption(grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20)))}
|
||||
allOpts = append(allOpts, opts...)
|
||||
// Prepare gRPC channels.
|
||||
if config.NumChannels == 0 {
|
||||
@@ -138,8 +138,12 @@ func NewClientWithConfig(ctx context.Context, database string, config ClientConf
|
||||
if config.MaxBurst == 0 {
|
||||
config.MaxBurst = 10
|
||||
}
|
||||
// Default MaxSessionAge
|
||||
if config.maxSessionAge == 0 {
|
||||
config.maxSessionAge = time.Minute * 30
|
||||
}
|
||||
for i := 0; i < config.NumChannels; i++ {
|
||||
conn, err := transport.DialGRPC(ctx, allOpts...)
|
||||
conn, err := gtransport.Dial(ctx, allOpts...)
|
||||
if err != nil {
|
||||
return nil, errDial(i, err)
|
||||
}
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
// Test validDatabaseName()
|
||||
func TestValidDatabaseName(t *testing.T) {
|
||||
validDbUri := "projects/spanner-cloud-test/instances/foo/databases/foodb"
|
||||
validDbURI := "projects/spanner-cloud-test/instances/foo/databases/foodb"
|
||||
invalidDbUris := []string{
|
||||
// Completely wrong DB URI.
|
||||
"foobarDB",
|
||||
@@ -32,12 +32,12 @@ func TestValidDatabaseName(t *testing.T) {
|
||||
// No instance ID.
|
||||
"projects/spanner-cloud-test/instances//databases/foodb",
|
||||
}
|
||||
if err := validDatabaseName(validDbUri); err != nil {
|
||||
t.Errorf("validateDatabaseName(%q) = %v, want nil", validDbUri, err)
|
||||
if err := validDatabaseName(validDbURI); err != nil {
|
||||
t.Errorf("validateDatabaseName(%q) = %v, want nil", validDbURI, err)
|
||||
}
|
||||
for _, d := range invalidDbUris {
|
||||
if err, wantErr := validDatabaseName(d), "should conform to pattern"; !strings.Contains(err.Error(), wantErr) {
|
||||
t.Errorf("validateDatabaseName(%q) = %q, want error pattern %q", validDbUri, err, wantErr)
|
||||
t.Errorf("validateDatabaseName(%q) = %q, want error pattern %q", validDbURI, err, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@ Package spanner provides a client for reading and writing to Cloud Spanner
|
||||
databases. See the packages under admin for clients that operate on databases
|
||||
and instances.
|
||||
|
||||
Note: This package is in alpha. Backwards-incompatible changes may occur
|
||||
without notice.
|
||||
Note: This package is in beta. Some backwards-incompatible changes may occur.
|
||||
|
||||
See https://cloud.google.com/spanner/docs/getting-started/go/ for an introduction
|
||||
to Cloud Spanner and additional help on using this API.
|
||||
|
||||
+38
-6
@@ -54,18 +54,23 @@ type MockCloudSpannerClient struct {
|
||||
sessions map[string]bool
|
||||
// Expected set of actions that will be executed by the client.
|
||||
actions []Action
|
||||
// Session ping history
|
||||
// Session ping history.
|
||||
pings []string
|
||||
// Injected error, will be returned by all APIs
|
||||
// Injected error, will be returned by all APIs.
|
||||
injErr map[string]error
|
||||
// nice client will not fail on any request
|
||||
// Client will not fail on any request.
|
||||
nice bool
|
||||
// Client will stall on any requests.
|
||||
freezed chan struct{}
|
||||
}
|
||||
|
||||
// NewMockCloudSpannerClient creates new MockCloudSpannerClient instance.
|
||||
func NewMockCloudSpannerClient(t *testing.T, acts ...Action) *MockCloudSpannerClient {
|
||||
mc := &MockCloudSpannerClient{t: t, sessions: map[string]bool{}, injErr: map[string]error{}}
|
||||
mc.SetActions(acts...)
|
||||
// Produce a closed channel, so the default action of ready is to not block.
|
||||
mc.Freeze()
|
||||
mc.Unfreeze()
|
||||
return mc
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ func (m *MockCloudSpannerClient) DumpSessions() map[string]bool {
|
||||
|
||||
// CreateSession is a placeholder for SpannerClient.CreateSession.
|
||||
func (m *MockCloudSpannerClient) CreateSession(c context.Context, r *sppb.CreateSessionRequest, opts ...grpc.CallOption) (*sppb.Session, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.injErr["CreateSession"]; err != nil {
|
||||
@@ -139,6 +145,7 @@ func (m *MockCloudSpannerClient) CreateSession(c context.Context, r *sppb.Create
|
||||
|
||||
// GetSession is a placeholder for SpannerClient.GetSession.
|
||||
func (m *MockCloudSpannerClient) GetSession(c context.Context, r *sppb.GetSessionRequest, opts ...grpc.CallOption) (*sppb.Session, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.injErr["GetSession"]; err != nil {
|
||||
@@ -153,6 +160,7 @@ func (m *MockCloudSpannerClient) GetSession(c context.Context, r *sppb.GetSessio
|
||||
|
||||
// DeleteSession is a placeholder for SpannerClient.DeleteSession.
|
||||
func (m *MockCloudSpannerClient) DeleteSession(c context.Context, r *sppb.DeleteSessionRequest, opts ...grpc.CallOption) (*empty.Empty, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.injErr["DeleteSession"]; err != nil {
|
||||
@@ -169,11 +177,13 @@ func (m *MockCloudSpannerClient) DeleteSession(c context.Context, r *sppb.Delete
|
||||
|
||||
// ExecuteSql is a placeholder for SpannerClient.ExecuteSql.
|
||||
func (m *MockCloudSpannerClient) ExecuteSql(c context.Context, r *sppb.ExecuteSqlRequest, opts ...grpc.CallOption) (*sppb.ResultSet, error) {
|
||||
m.ready()
|
||||
return nil, errors.New("Unimplemented")
|
||||
}
|
||||
|
||||
// ExecuteStreamingSql is a mock implementation of SpannerClient.ExecuteStreamingSql.
|
||||
func (m *MockCloudSpannerClient) ExecuteStreamingSql(c context.Context, r *sppb.ExecuteSqlRequest, opts ...grpc.CallOption) (sppb.Spanner_ExecuteStreamingSqlClient, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.injErr["ExecuteStreamingSql"]; err != nil {
|
||||
@@ -220,12 +230,14 @@ func (m *MockCloudSpannerClient) ExecuteStreamingSql(c context.Context, r *sppb.
|
||||
|
||||
// Read is a placeholder for SpannerClient.Read.
|
||||
func (m *MockCloudSpannerClient) Read(c context.Context, r *sppb.ReadRequest, opts ...grpc.CallOption) (*sppb.ResultSet, error) {
|
||||
m.ready()
|
||||
m.t.Fatalf("Read is unimplemented")
|
||||
return nil, errors.New("Unimplemented")
|
||||
}
|
||||
|
||||
// StreamingRead is a placeholder for SpannerClient.StreamingRead.
|
||||
func (m *MockCloudSpannerClient) StreamingRead(c context.Context, r *sppb.ReadRequest, opts ...grpc.CallOption) (sppb.Spanner_StreamingReadClient, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.injErr["StreamingRead"]; err != nil {
|
||||
@@ -258,15 +270,15 @@ func (m *MockCloudSpannerClient) StreamingRead(c context.Context, r *sppb.ReadRe
|
||||
Table: "t_mock",
|
||||
Columns: []string{"col1", "col2"},
|
||||
KeySet: &sppb.KeySet{
|
||||
[]*proto3.ListValue{
|
||||
Keys: []*proto3.ListValue{
|
||||
&proto3.ListValue{
|
||||
Values: []*proto3.Value{
|
||||
&proto3.Value{Kind: &proto3.Value_StringValue{StringValue: "foo"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*sppb.KeyRange{},
|
||||
false,
|
||||
Ranges: []*sppb.KeyRange{},
|
||||
All: false,
|
||||
},
|
||||
}
|
||||
if act.method == "StreamingIndexRead" {
|
||||
@@ -283,6 +295,7 @@ func (m *MockCloudSpannerClient) StreamingRead(c context.Context, r *sppb.ReadRe
|
||||
|
||||
// BeginTransaction is a placeholder for SpannerClient.BeginTransaction.
|
||||
func (m *MockCloudSpannerClient) BeginTransaction(c context.Context, r *sppb.BeginTransactionRequest, opts ...grpc.CallOption) (*sppb.Transaction, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.nice {
|
||||
@@ -310,6 +323,7 @@ func (m *MockCloudSpannerClient) BeginTransaction(c context.Context, r *sppb.Beg
|
||||
|
||||
// Commit is a placeholder for SpannerClient.Commit.
|
||||
func (m *MockCloudSpannerClient) Commit(c context.Context, r *sppb.CommitRequest, opts ...grpc.CallOption) (*sppb.CommitResponse, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.nice {
|
||||
@@ -333,6 +347,7 @@ func (m *MockCloudSpannerClient) Commit(c context.Context, r *sppb.CommitRequest
|
||||
|
||||
// Rollback is a placeholder for SpannerClient.Rollback.
|
||||
func (m *MockCloudSpannerClient) Rollback(c context.Context, r *sppb.RollbackRequest, opts ...grpc.CallOption) (*empty.Empty, error) {
|
||||
m.ready()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.nice {
|
||||
@@ -353,3 +368,20 @@ func (m *MockCloudSpannerClient) Rollback(c context.Context, r *sppb.RollbackReq
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Freeze stalls all requests.
|
||||
func (m *MockCloudSpannerClient) Freeze() {
|
||||
m.freezed = make(chan struct{})
|
||||
}
|
||||
|
||||
// Unfreeze restores processing requests.
|
||||
func (m *MockCloudSpannerClient) Unfreeze() {
|
||||
close(m.freezed)
|
||||
}
|
||||
|
||||
// ready checks conditions before executing requests
|
||||
// TODO: also check injected errors, actions
|
||||
func (m *MockCloudSpannerClient) ready() {
|
||||
// check if client should be freezed
|
||||
<-m.freezed
|
||||
}
|
||||
|
||||
+3
-3
@@ -155,10 +155,10 @@ func (key Key) String() string {
|
||||
}
|
||||
|
||||
// AsPrefix returns a KeyRange for all keys where k is the prefix.
|
||||
func (k Key) AsPrefix() KeyRange {
|
||||
func (key Key) AsPrefix() KeyRange {
|
||||
return KeyRange{
|
||||
Start: k,
|
||||
End: k,
|
||||
Start: key,
|
||||
End: key,
|
||||
Kind: ClosedClosed,
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -209,32 +209,32 @@ func TestKeyRange(t *testing.T) {
|
||||
{
|
||||
kr: KeyRange{Key{"A"}, Key{"D"}, OpenOpen},
|
||||
wantProto: &sppb.KeyRange{
|
||||
&sppb.KeyRange_StartOpen{listValueProto(stringProto("A"))},
|
||||
&sppb.KeyRange_EndOpen{listValueProto(stringProto("D"))},
|
||||
StartKeyType: &sppb.KeyRange_StartOpen{StartOpen: listValueProto(stringProto("A"))},
|
||||
EndKeyType: &sppb.KeyRange_EndOpen{EndOpen: listValueProto(stringProto("D"))},
|
||||
},
|
||||
wantStr: `(("A"),("D"))`,
|
||||
},
|
||||
{
|
||||
kr: KeyRange{Key{1}, Key{10}, OpenClosed},
|
||||
wantProto: &sppb.KeyRange{
|
||||
&sppb.KeyRange_StartOpen{listValueProto(stringProto("1"))},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(stringProto("10"))},
|
||||
StartKeyType: &sppb.KeyRange_StartOpen{StartOpen: listValueProto(stringProto("1"))},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(stringProto("10"))},
|
||||
},
|
||||
wantStr: "((1),(10)]",
|
||||
},
|
||||
{
|
||||
kr: KeyRange{Key{1.5, 2.1, 0.2}, Key{1.9, 0.7}, ClosedOpen},
|
||||
wantProto: &sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(floatProto(1.5), floatProto(2.1), floatProto(0.2))},
|
||||
&sppb.KeyRange_EndOpen{listValueProto(floatProto(1.9), floatProto(0.7))},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(floatProto(1.5), floatProto(2.1), floatProto(0.2))},
|
||||
EndKeyType: &sppb.KeyRange_EndOpen{EndOpen: listValueProto(floatProto(1.9), floatProto(0.7))},
|
||||
},
|
||||
wantStr: "[(1.5,2.1,0.2),(1.9,0.7))",
|
||||
},
|
||||
{
|
||||
kr: KeyRange{Key{NullInt64{1, true}}, Key{10}, ClosedClosed},
|
||||
wantProto: &sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(stringProto("1"))},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(stringProto("10"))},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(stringProto("1"))},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(stringProto("10"))},
|
||||
},
|
||||
wantStr: "[(1),(10)]",
|
||||
},
|
||||
@@ -296,8 +296,8 @@ func TestKeySets(t *testing.T) {
|
||||
KeyRange{Key{1}, Key{2}, ClosedOpen},
|
||||
&sppb.KeySet{Ranges: []*sppb.KeyRange{
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(int1)},
|
||||
&sppb.KeyRange_EndOpen{listValueProto(int2)},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(int1)},
|
||||
EndKeyType: &sppb.KeyRange_EndOpen{EndOpen: listValueProto(int2)},
|
||||
},
|
||||
}},
|
||||
},
|
||||
@@ -305,8 +305,8 @@ func TestKeySets(t *testing.T) {
|
||||
Key{2}.AsPrefix(),
|
||||
&sppb.KeySet{Ranges: []*sppb.KeyRange{
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(int2)},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(int2)},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(int2)},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(int2)},
|
||||
},
|
||||
}},
|
||||
},
|
||||
@@ -318,12 +318,12 @@ func TestKeySets(t *testing.T) {
|
||||
&sppb.KeySet{
|
||||
Ranges: []*sppb.KeyRange{
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(int1)},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(int2)},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(int1)},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(int2)},
|
||||
},
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartOpen{listValueProto(int3)},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(int4)},
|
||||
StartKeyType: &sppb.KeyRange_StartOpen{StartOpen: listValueProto(int3)},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(int4)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -342,12 +342,12 @@ func TestKeySets(t *testing.T) {
|
||||
},
|
||||
Ranges: []*sppb.KeyRange{
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartClosed{listValueProto(int2)},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(int3)},
|
||||
StartKeyType: &sppb.KeyRange_StartClosed{StartClosed: listValueProto(int2)},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(int3)},
|
||||
},
|
||||
&sppb.KeyRange{
|
||||
&sppb.KeyRange_StartOpen{listValueProto(int4)},
|
||||
&sppb.KeyRange_EndClosed{listValueProto(intProto(5))},
|
||||
StartKeyType: &sppb.KeyRange_StartOpen{StartOpen: listValueProto(int4)},
|
||||
EndKeyType: &sppb.KeyRange_EndClosed{EndClosed: listValueProto(intProto(5))},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ type Mutation struct {
|
||||
// op is the operation type of the mutation.
|
||||
// See documentation for spanner.op for more details.
|
||||
op op
|
||||
// Table is the name of the taget table to be modified.
|
||||
// Table is the name of the target table to be modified.
|
||||
table string
|
||||
// keySet is a set of primary keys that names the rows
|
||||
// in a delete operation.
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ func listType(t *sppb.Type) *sppb.Type {
|
||||
}
|
||||
|
||||
func mkField(n string, t *sppb.Type) *sppb.StructType_Field {
|
||||
return &sppb.StructType_Field{n, t}
|
||||
return &sppb.StructType_Field{Name: n, Type: t}
|
||||
}
|
||||
|
||||
func structType(fields ...*sppb.StructType_Field) *sppb.Type {
|
||||
|
||||
+6
-6
@@ -50,19 +50,19 @@ import (
|
||||
// Supported types and their corresponding Cloud Spanner column type(s) are:
|
||||
//
|
||||
// *string(not NULL), *NullString - STRING
|
||||
// *[]NullString - STRING ARRAY
|
||||
// *[]string, *[]NullString - STRING ARRAY
|
||||
// *[]byte - BYTES
|
||||
// *[][]byte - BYTES ARRAY
|
||||
// *int64(not NULL), *NullInt64 - INT64
|
||||
// *[]NullInt64 - INT64 ARRAY
|
||||
// *[]int64, *[]NullInt64 - INT64 ARRAY
|
||||
// *bool(not NULL), *NullBool - BOOL
|
||||
// *[]NullBool - BOOL ARRAY
|
||||
// *[]bool, *[]NullBool - BOOL ARRAY
|
||||
// *float64(not NULL), *NullFloat64 - FLOAT64
|
||||
// *[]NullFloat64 - FLOAT64 ARRAY
|
||||
// *[]float64, *[]NullFloat64 - FLOAT64 ARRAY
|
||||
// *time.Time(not NULL), *NullTime - TIMESTAMP
|
||||
// *[]NullTime - TIMESTAMP ARRAY
|
||||
// *[]time.Time, *[]NullTime - TIMESTAMP ARRAY
|
||||
// *Date(not NULL), *NullDate - DATE
|
||||
// *[]NullDate - DATE ARRAY
|
||||
// *[]civil.Date, *[]NullDate - DATE ARRAY
|
||||
// *[]*some_go_struct, *[]NullRow - STRUCT ARRAY
|
||||
// *GenericColumnValue - any Cloud Spanner type
|
||||
//
|
||||
|
||||
+15
-6
@@ -413,11 +413,11 @@ func TestColumnTypeErr(t *testing.T) {
|
||||
badDst := &struct{}{}
|
||||
for i, f := range row.fields { // For each of the columns, try to decode it into badDst.
|
||||
tc := f.Type.Code
|
||||
isArray := strings.Contains(f.Name, "ARRAY")
|
||||
if isArray {
|
||||
tc = f.Type.ArrayElementType.Code
|
||||
var etc sppb.TypeCode
|
||||
if strings.Contains(f.Name, "ARRAY") {
|
||||
etc = f.Type.ArrayElementType.Code
|
||||
}
|
||||
wantErr := errDecodeColumn(i, errTypeMismatch(tc, isArray, badDst))
|
||||
wantErr := errDecodeColumn(i, errTypeMismatch(tc, etc, badDst))
|
||||
if gotErr := row.Column(i, badDst); !reflect.DeepEqual(gotErr, wantErr) {
|
||||
t.Errorf("Column(%v): decoding into destination with wrong type %T returns error %v, want %v",
|
||||
i, badDst, gotErr, wantErr)
|
||||
@@ -427,7 +427,7 @@ func TestColumnTypeErr(t *testing.T) {
|
||||
f.Name, badDst, gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
wantErr := errDecodeColumn(1, errTypeMismatch(sppb.TypeCode_STRING, false, badDst))
|
||||
wantErr := errDecodeColumn(1, errTypeMismatch(sppb.TypeCode_STRING, sppb.TypeCode_TYPE_CODE_UNSPECIFIED, badDst))
|
||||
// badDst is used to receive column 1.
|
||||
vals := []interface{}{nil, badDst} // Row.Column() is expected to fail at column 1.
|
||||
// Skip decoding the rest columns by providing nils as the destinations.
|
||||
@@ -571,7 +571,7 @@ func TestToStructInvalidDst(t *testing.T) {
|
||||
PK1 int64 `spanner:"STRING"`
|
||||
}{},
|
||||
errDecodeStructField(&sppb.StructType{Fields: row.fields}, "STRING",
|
||||
errTypeMismatch(sppb.TypeCode_STRING, false, proto.Int64(0))),
|
||||
errTypeMismatch(sppb.TypeCode_STRING, sppb.TypeCode_TYPE_CODE_UNSPECIFIED, proto.Int64(0))),
|
||||
},
|
||||
} {
|
||||
if gotErr := row.ToStruct(test.dst); !reflect.DeepEqual(gotErr, test.wantErr) {
|
||||
@@ -1773,3 +1773,12 @@ func TestNewRow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkColumn(b *testing.B) {
|
||||
var s string
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := row.Column(0, &s); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+190
-32
@@ -201,6 +201,7 @@ func (s *session) refreshIdle() bool {
|
||||
if s.valid && s.idleList != nil {
|
||||
// session is in idle list, refresh its session id.
|
||||
sid, s.id = s.id, sid
|
||||
s.createTime = time.Now()
|
||||
if s.tx != nil {
|
||||
s.tx = nil
|
||||
s.pool.idleWriteList.Remove(s.idleList)
|
||||
@@ -351,17 +352,23 @@ type SessionPoolConfig struct {
|
||||
// to be broken, it will still be evicted from session pool, therefore it is
|
||||
// posssible that the number of opened sessions drops below MinOpened.
|
||||
MinOpened uint64
|
||||
// MaxSessionAge is the maximum duration that a session can be reused, zero
|
||||
// maxSessionAge is the maximum duration that a session can be reused, zero
|
||||
// means session pool will never expire sessions.
|
||||
MaxSessionAge time.Duration
|
||||
maxSessionAge time.Duration
|
||||
// MaxIdle is the maximum number of idle sessions, pool is allowed to keep. Defaults to 0.
|
||||
MaxIdle uint64
|
||||
// MaxBurst is the maximum number of concurrent session creation requests. Defaults to 10.
|
||||
MaxBurst uint64
|
||||
// WriteSessions is the fraction of sessions we try to keep prepared for write.
|
||||
WriteSessions float64
|
||||
// HealthCheckWorkers is number of workers used by health checker for this pool.
|
||||
HealthCheckWorkers int
|
||||
// HealthCheckInterval is how often the health checker pings a session.
|
||||
// HealthCheckInterval is how often the health checker pings a session. Defaults to 5 min.
|
||||
HealthCheckInterval time.Duration
|
||||
// healthCheckMaintainerEnabled enables the session pool maintainer.
|
||||
healthCheckMaintainerEnabled bool
|
||||
// healthCheckSampleInterval is how often the health checker samples live session (for use in maintaining session pool size). Defaults to 1 min.
|
||||
healthCheckSampleInterval time.Duration
|
||||
}
|
||||
|
||||
// errNoRPCGetter returns error for SessionPoolConfig missing getRPCClient method.
|
||||
@@ -436,9 +443,13 @@ func newSessionPool(db string, config SessionPoolConfig, md metadata.MD) (*sessi
|
||||
if config.HealthCheckInterval == 0 {
|
||||
config.HealthCheckInterval = 5 * time.Minute
|
||||
}
|
||||
if config.healthCheckSampleInterval == 0 {
|
||||
config.healthCheckSampleInterval = time.Minute
|
||||
}
|
||||
// On GCE VM, within the same region an healthcheck ping takes on average 10ms to finish, given a 5 minutes interval and
|
||||
// 10 healthcheck workers, a healthChecker can effectively mantain 100 checks_per_worker/sec * 10 workers * 300 seconds = 300K sessions.
|
||||
pool.hc = newHealthChecker(config.HealthCheckInterval, config.HealthCheckWorkers, pool)
|
||||
pool.hc = newHealthChecker(config.HealthCheckInterval, config.HealthCheckWorkers, config.healthCheckSampleInterval, pool)
|
||||
close(pool.hc.ready)
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
@@ -666,15 +677,15 @@ func (p *sessionPool) recycle(s *session) bool {
|
||||
// Reject the session if session is invalid or pool itself is invalid.
|
||||
return false
|
||||
}
|
||||
if p.MaxSessionAge != 0 && s.createTime.Add(p.MaxSessionAge).Before(time.Now()) && p.numOpened > p.MinOpened {
|
||||
// session expires and number of opened sessions exceeds MinOpened, let the session destroy itself.
|
||||
if p.maxSessionAge != 0 && s.createTime.Add(p.maxSessionAge).Before(time.Now()) && p.numOpened > p.MinOpened {
|
||||
// session expires and number of opened sessions exceeds MinOpened, let the session itself.
|
||||
return false
|
||||
}
|
||||
// Hot sessions will be converging at the front of the list, cold sessions will be evicted by healthcheck workers.
|
||||
// Put session at the back of the list to round robin for load balancing across channels.
|
||||
if s.isWritePrepared() {
|
||||
s.setIdleList(p.idleWriteList.PushFront(s))
|
||||
s.setIdleList(p.idleWriteList.PushBack(s))
|
||||
} else {
|
||||
s.setIdleList(p.idleList.PushFront(s))
|
||||
s.setIdleList(p.idleList.PushBack(s))
|
||||
}
|
||||
// Broadcast that a session has been returned to idle list.
|
||||
close(p.mayGetSession)
|
||||
@@ -762,21 +773,34 @@ type healthChecker struct {
|
||||
waitWorkers sync.WaitGroup
|
||||
// pool is the underlying session pool.
|
||||
pool *sessionPool
|
||||
// closed marks if a healthChecker has been closed.
|
||||
closed bool
|
||||
// sampleInterval is the interval of sampling by the maintainer.
|
||||
sampleInterval time.Duration
|
||||
// ready is used to signal that maintainer can start running.
|
||||
ready chan struct{}
|
||||
// done is used to signal that health checker should be closed.
|
||||
done chan struct{}
|
||||
// once is used for closing channel done only once.
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// newHealthChecker initializes new instance of healthChecker.
|
||||
func newHealthChecker(interval time.Duration, workers int, pool *sessionPool) *healthChecker {
|
||||
func newHealthChecker(interval time.Duration, workers int, sampleInterval time.Duration, pool *sessionPool) *healthChecker {
|
||||
if workers <= 0 {
|
||||
workers = 1
|
||||
}
|
||||
hc := &healthChecker{
|
||||
interval: interval,
|
||||
workers: workers,
|
||||
pool: pool,
|
||||
interval: interval,
|
||||
workers: workers,
|
||||
pool: pool,
|
||||
sampleInterval: sampleInterval,
|
||||
ready: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
for i := 0; i < hc.workers; i++ {
|
||||
if hc.pool.healthCheckMaintainerEnabled {
|
||||
hc.waitWorkers.Add(1)
|
||||
go hc.maintainer()
|
||||
}
|
||||
for i := 1; i <= hc.workers; i++ {
|
||||
hc.waitWorkers.Add(1)
|
||||
go hc.worker(i)
|
||||
}
|
||||
@@ -785,17 +809,18 @@ func newHealthChecker(interval time.Duration, workers int, pool *sessionPool) *h
|
||||
|
||||
// close closes the healthChecker and waits for all healthcheck workers to exit.
|
||||
func (hc *healthChecker) close() {
|
||||
hc.mu.Lock()
|
||||
hc.closed = true
|
||||
hc.mu.Unlock()
|
||||
hc.once.Do(func() { close(hc.done) })
|
||||
hc.waitWorkers.Wait()
|
||||
}
|
||||
|
||||
// isClosing checks if a healthChecker is already closing.
|
||||
func (hc *healthChecker) isClosing() bool {
|
||||
hc.mu.Lock()
|
||||
defer hc.mu.Unlock()
|
||||
return hc.closed
|
||||
select {
|
||||
case <-hc.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getInterval gets the healthcheck interval.
|
||||
@@ -856,7 +881,7 @@ func (hc *healthChecker) healthCheck(s *session) {
|
||||
s.destroy(false)
|
||||
return
|
||||
}
|
||||
if s.pool.MaxSessionAge != 0 && s.createTime.Add(s.pool.MaxSessionAge).Before(time.Now()) {
|
||||
if s.pool.maxSessionAge != 0 && s.createTime.Add(s.pool.maxSessionAge).Before(time.Now()) {
|
||||
// Session reaches its maximum age, retire it. Failing that try to refresh it.
|
||||
if s.destroy(true) || !s.refreshIdle() {
|
||||
return
|
||||
@@ -870,9 +895,6 @@ func (hc *healthChecker) healthCheck(s *session) {
|
||||
|
||||
// worker performs the healthcheck on sessions in healthChecker's priority queue.
|
||||
func (hc *healthChecker) worker(i int) {
|
||||
if log.V(2) {
|
||||
log.Infof("Starting health check worker %v", i)
|
||||
}
|
||||
// Returns a session which we should ping to keep it alive.
|
||||
getNextForPing := func() *session {
|
||||
hc.pool.mu.Lock()
|
||||
@@ -918,9 +940,6 @@ func (hc *healthChecker) worker(i int) {
|
||||
|
||||
for {
|
||||
if hc.isClosing() {
|
||||
if log.V(2) {
|
||||
log.Infof("Closing health check worker %v", i)
|
||||
}
|
||||
// Exit when the pool has been closed and all sessions have been destroyed
|
||||
// or when health checker has been closed.
|
||||
hc.waitWorkers.Done()
|
||||
@@ -929,8 +948,12 @@ func (hc *healthChecker) worker(i int) {
|
||||
ws := getNextForTx()
|
||||
if ws != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
ws.prepareForWrite(contextWithOutgoingMetadata(ctx, hc.pool.md))
|
||||
err := ws.prepareForWrite(contextWithOutgoingMetadata(ctx, hc.pool.md))
|
||||
cancel()
|
||||
if err != nil {
|
||||
// TODO(dixiao): handle error properly
|
||||
log.Errorf("prepareForWrite failed: %v", err)
|
||||
}
|
||||
hc.pool.recycle(ws)
|
||||
hc.pool.mu.Lock()
|
||||
hc.pool.prepareReqs--
|
||||
@@ -945,7 +968,13 @@ func (hc *healthChecker) worker(i int) {
|
||||
if pause > int64(hc.interval) {
|
||||
pause = int64(hc.interval)
|
||||
}
|
||||
<-time.After(time.Duration(rand.Int63n(pause) + pause/2))
|
||||
select {
|
||||
case <-time.After(time.Duration(rand.Int63n(pause) + pause/2)):
|
||||
break
|
||||
case <-hc.done:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -953,6 +982,135 @@ func (hc *healthChecker) worker(i int) {
|
||||
}
|
||||
}
|
||||
|
||||
// maintainer maintains the maxSessionsInUse by a window of kWindowSize * sampleInterval.
|
||||
// Based on this information, health checker will try to maintain the number of sessions by hc..
|
||||
func (hc *healthChecker) maintainer() {
|
||||
// Wait so that pool is ready.
|
||||
<-hc.ready
|
||||
|
||||
var (
|
||||
windowSize uint64 = 10
|
||||
iteration uint64
|
||||
timeout <-chan time.Time
|
||||
)
|
||||
|
||||
// replenishPool is run if numOpened is less than sessionsToKeep, timeouts on sampleInterval.
|
||||
replenishPool := func(sessionsToKeep uint64) {
|
||||
ctx, _ := context.WithTimeout(context.Background(), hc.sampleInterval)
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
p := hc.pool
|
||||
p.mu.Lock()
|
||||
// Take budget before the actual session creation.
|
||||
if sessionsToKeep <= p.numOpened {
|
||||
p.mu.Unlock()
|
||||
break
|
||||
}
|
||||
p.numOpened++
|
||||
p.createReqs++
|
||||
shouldPrepareWrite := p.shouldPrepareWrite()
|
||||
p.mu.Unlock()
|
||||
var (
|
||||
s *session
|
||||
err error
|
||||
)
|
||||
if s, err = p.createSession(ctx); err != nil {
|
||||
log.Warningf("Failed to create session, error: %v", toSpannerError(err))
|
||||
continue
|
||||
}
|
||||
if shouldPrepareWrite {
|
||||
if err = s.prepareForWrite(ctx); err != nil {
|
||||
log.Warningf("Failed to prepare session, error: %v", toSpannerError(err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
p.recycle(s)
|
||||
}
|
||||
}
|
||||
|
||||
// shrinkPool, scales down the session pool.
|
||||
shrinkPool := func(sessionsToKeep uint64) {
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
p := hc.pool
|
||||
p.mu.Lock()
|
||||
|
||||
if sessionsToKeep >= p.numOpened {
|
||||
p.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
var s *session
|
||||
if p.idleList.Len() > 0 {
|
||||
s = p.idleList.Front().Value.(*session)
|
||||
} else if p.idleWriteList.Len() > 0 {
|
||||
s = p.idleWriteList.Front().Value.(*session)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
if s != nil {
|
||||
// destroy session as expire.
|
||||
s.destroy(true)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if hc.isClosing() {
|
||||
hc.waitWorkers.Done()
|
||||
return
|
||||
}
|
||||
|
||||
// maxSessionsInUse is the maximum number of sessions in use concurrently over a period of time.
|
||||
var maxSessionsInUse uint64
|
||||
|
||||
// Updates metrics.
|
||||
hc.pool.mu.Lock()
|
||||
currSessionsInUse := hc.pool.numOpened - uint64(hc.pool.idleList.Len()) - uint64(hc.pool.idleWriteList.Len())
|
||||
currSessionsOpened := hc.pool.numOpened
|
||||
hc.pool.mu.Unlock()
|
||||
|
||||
hc.mu.Lock()
|
||||
if iteration%windowSize == 0 || maxSessionsInUse < currSessionsInUse {
|
||||
maxSessionsInUse = currSessionsInUse
|
||||
}
|
||||
sessionsToKeep := maxUint64(hc.pool.MinOpened,
|
||||
minUint64(currSessionsOpened, hc.pool.MaxIdle+maxSessionsInUse))
|
||||
hc.mu.Unlock()
|
||||
|
||||
timeout = time.After(hc.sampleInterval)
|
||||
// Replenish or Shrink pool if needed.
|
||||
// Note: we don't need to worry about pending create session requests, we only need to sample the current sessions in use.
|
||||
// the routines will not try to create extra / delete creating sessions.
|
||||
if sessionsToKeep > currSessionsOpened {
|
||||
replenishPool(sessionsToKeep)
|
||||
} else {
|
||||
shrinkPool(sessionsToKeep)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-timeout:
|
||||
break
|
||||
case <-hc.done:
|
||||
break
|
||||
}
|
||||
iteration++
|
||||
}
|
||||
}
|
||||
|
||||
// shouldDropSession returns true if a particular error leads to the removal of a session
|
||||
func shouldDropSession(err error) bool {
|
||||
if err == nil {
|
||||
|
||||
+125
-16
@@ -38,7 +38,13 @@ func setup(t *testing.T, spc SessionPoolConfig) (sp *sessionPool, sc *testutil.M
|
||||
spc.getRPCClient = func() (sppb.SpannerClient, error) {
|
||||
return sc, nil
|
||||
}
|
||||
spc.HealthCheckInterval = 50 * time.Millisecond
|
||||
if spc.HealthCheckInterval == 0 {
|
||||
spc.HealthCheckInterval = 50 * time.Millisecond
|
||||
}
|
||||
if spc.healthCheckSampleInterval == 0 {
|
||||
spc.healthCheckSampleInterval = 10 * time.Millisecond
|
||||
}
|
||||
spc.healthCheckMaintainerEnabled = true
|
||||
sp, err := newSessionPool("mockdb", spc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create session pool: %v", err)
|
||||
@@ -51,6 +57,7 @@ func setup(t *testing.T, spc SessionPoolConfig) (sp *sessionPool, sc *testutil.M
|
||||
|
||||
// TestSessionCreation tests session creation during sessionPool.Take().
|
||||
func TestSessionCreation(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{})
|
||||
defer cancel()
|
||||
// Take three sessions from session pool, this should trigger session pool to create three new sessions.
|
||||
@@ -96,7 +103,8 @@ func TestSessionCreation(t *testing.T) {
|
||||
|
||||
// TestTakeFromIdleList tests taking sessions from session pool's idle list.
|
||||
func TestTakeFromIdleList(t *testing.T) {
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{})
|
||||
t.Parallel()
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxIdle: 10}) // make sure maintainer keeps the idle sessions
|
||||
defer cancel()
|
||||
// Take ten sessions from session pool and recycle them.
|
||||
shs := make([]*sessionHandle, 10)
|
||||
@@ -107,6 +115,8 @@ func TestTakeFromIdleList(t *testing.T) {
|
||||
t.Errorf("failed to get session(%v): %v", i, err)
|
||||
}
|
||||
}
|
||||
// Make sure it's sampled once before recycling, otherwise it will be cleaned up.
|
||||
<-time.After(sp.SessionPoolConfig.healthCheckSampleInterval)
|
||||
for i := 0; i < len(shs); i++ {
|
||||
shs[i].recycle()
|
||||
}
|
||||
@@ -131,7 +141,8 @@ func TestTakeFromIdleList(t *testing.T) {
|
||||
|
||||
// TesttakeWriteSessionFromIdleList tests taking write sessions from session pool's idle list.
|
||||
func TestTakeWriteSessionFromIdleList(t *testing.T) {
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{})
|
||||
t.Parallel()
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxIdle: 20}) // make sure maintainer keeps the idle sessions
|
||||
defer cancel()
|
||||
act := testutil.NewAction("Begin", nil)
|
||||
acts := make([]testutil.Action, 20)
|
||||
@@ -148,6 +159,8 @@ func TestTakeWriteSessionFromIdleList(t *testing.T) {
|
||||
t.Errorf("failed to get session(%v): %v", i, err)
|
||||
}
|
||||
}
|
||||
// Make sure it's sampled once before recycling, otherwise it will be cleaned up.
|
||||
<-time.After(sp.SessionPoolConfig.healthCheckSampleInterval)
|
||||
for i := 0; i < len(shs); i++ {
|
||||
shs[i].recycle()
|
||||
}
|
||||
@@ -172,10 +185,11 @@ func TestTakeWriteSessionFromIdleList(t *testing.T) {
|
||||
|
||||
// TestTakeFromIdleListChecked tests taking sessions from session pool's idle list, but with a extra ping check.
|
||||
func TestTakeFromIdleListChecked(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{})
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxIdle: 1}) // make sure maintainer keeps the idle sessions
|
||||
defer cancel()
|
||||
// Stop healthcheck workers to simulate slow pings.
|
||||
sp.hc.close()
|
||||
@@ -184,6 +198,8 @@ func TestTakeFromIdleListChecked(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("failed to get session: %v", err)
|
||||
}
|
||||
// Make sure it's sampled once before recycling, otherwise it will be cleaned up.
|
||||
<-time.After(sp.SessionPoolConfig.healthCheckSampleInterval)
|
||||
wantSid := sh.getID()
|
||||
sh.recycle()
|
||||
<-time.After(time.Second)
|
||||
@@ -225,10 +241,11 @@ func TestTakeFromIdleListChecked(t *testing.T) {
|
||||
|
||||
// TestTakeFromIdleWriteListChecked tests taking sessions from session pool's idle list, but with a extra ping check.
|
||||
func TestTakeFromIdleWriteListChecked(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{})
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxIdle: 1}) // make sure maintainer keeps the idle sessions
|
||||
defer cancel()
|
||||
sc.MakeNice()
|
||||
// Stop healthcheck workers to simulate slow pings.
|
||||
@@ -239,6 +256,8 @@ func TestTakeFromIdleWriteListChecked(t *testing.T) {
|
||||
t.Errorf("failed to get session: %v", err)
|
||||
}
|
||||
wantSid := sh.getID()
|
||||
// Make sure it's sampled once before recycling, otherwise it will be cleaned up.
|
||||
<-time.After(sp.SessionPoolConfig.healthCheckSampleInterval)
|
||||
sh.recycle()
|
||||
<-time.After(time.Second)
|
||||
// Two back-to-back session requests, both of them should return the same session created before and
|
||||
@@ -279,6 +298,7 @@ func TestTakeFromIdleWriteListChecked(t *testing.T) {
|
||||
|
||||
// TestMaxOpenedSessions tests max open sessions constraint.
|
||||
func TestMaxOpenedSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
@@ -337,12 +357,13 @@ func TestMinOpenedSessions(t *testing.T) {
|
||||
defer sp.mu.Unlock()
|
||||
// There should be still one session left in idle list due to the min open sessions constraint.
|
||||
if sp.idleList.Len() != 1 {
|
||||
t.Errorf("got %v sessions in idle list, want 1", sp.idleList.Len())
|
||||
t.Errorf("got %v sessions in idle list, want 1 %d", sp.idleList.Len(), sp.numOpened)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaxBurst tests max burst constraint.
|
||||
func TestMaxBurst(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
@@ -385,13 +406,29 @@ func TestMaxBurst(t *testing.T) {
|
||||
|
||||
// TestSessionrecycle tests recycling sessions.
|
||||
func TestSessionRecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, _, cancel := setup(t, SessionPoolConfig{MaxSessionAge: 100 * time.Millisecond, MinOpened: 1})
|
||||
sp, _, cancel := setup(t, SessionPoolConfig{maxSessionAge: 100 * time.Millisecond, MinOpened: 1, MaxIdle: 2})
|
||||
// Set MaxIdle to ensure shs[0] is not destroyed from scale down.
|
||||
// Healthcheck is explicitly turned off in this test because it might aggressively expire sessions in idle list.
|
||||
sp.hc.close()
|
||||
defer cancel()
|
||||
|
||||
// Test session is correctly recycled and reused.
|
||||
for i := 0; i < 20; i++ {
|
||||
s, err := sp.take(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("cannot get the session %v: %v", i, err)
|
||||
}
|
||||
s.recycle()
|
||||
}
|
||||
if sp.numOpened != 1 {
|
||||
t.Errorf("Expect session pool size %d, got %d", 1, sp.numOpened)
|
||||
}
|
||||
|
||||
// Test recycling expired session.
|
||||
var ss []*session
|
||||
shs := make([]*sessionHandle, 2)
|
||||
for i := 0; i < len(shs); i++ {
|
||||
@@ -419,6 +456,7 @@ func TestSessionRecycle(t *testing.T) {
|
||||
|
||||
// TestSessionDestroy tests destroying sessions.
|
||||
func TestSessionDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
sp, _, cancel := setup(t, SessionPoolConfig{MinOpened: 1})
|
||||
defer cancel()
|
||||
sh, err := sp.take(context.Background())
|
||||
@@ -471,6 +509,7 @@ func TestHcHeap(t *testing.T) {
|
||||
|
||||
// TestHealthCheckScheduler tests if healthcheck workers can schedule and perform healthchecks properly.
|
||||
func TestHealthCheckScheduler(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
@@ -506,7 +545,7 @@ func TestWriteSessionsPrepared(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{WriteSessions: 0.5})
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{WriteSessions: 0.5, MaxIdle: 20})
|
||||
sc.MakeNice()
|
||||
defer cancel()
|
||||
shs := make([]*sessionHandle, 10)
|
||||
@@ -557,10 +596,11 @@ func TestWriteSessionsPrepared(t *testing.T) {
|
||||
|
||||
// TestTakeFromWriteQueue tests that sessionPool.take() returns write prepared sessions as well.
|
||||
func TestTakeFromWriteQueue(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxOpened: 1, WriteSessions: 1.0})
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxOpened: 1, WriteSessions: 1.0, MaxIdle: 1})
|
||||
sc.MakeNice()
|
||||
defer cancel()
|
||||
sh, err := sp.take(context.Background())
|
||||
@@ -585,10 +625,11 @@ func TestTakeFromWriteQueue(t *testing.T) {
|
||||
|
||||
// TestSessionHealthCheck tests healthchecking cases.
|
||||
func TestSessionHealthCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{MaxSessionAge: 2 * time.Second})
|
||||
sp, sc, cancel := setup(t, SessionPoolConfig{maxSessionAge: 2 * time.Second})
|
||||
defer cancel()
|
||||
// Test pinging sessions.
|
||||
sh, err := sp.take(context.Background())
|
||||
@@ -632,7 +673,7 @@ func TestSessionHealthCheck(t *testing.T) {
|
||||
// Test session id refresh.
|
||||
// Recreate the session pool with min open sessions constraint.
|
||||
sp, err = newSessionPool("mockdb", SessionPoolConfig{
|
||||
MaxSessionAge: time.Second,
|
||||
maxSessionAge: time.Second,
|
||||
MinOpened: 1,
|
||||
getRPCClient: func() (sppb.SpannerClient, error) {
|
||||
return sc, nil
|
||||
@@ -667,21 +708,23 @@ func TestSessionHealthCheck(t *testing.T) {
|
||||
// During the test, it is expected that all sessions that are taken from session pool remains valid and
|
||||
// when all test workers and healthcheck workers exit, mockclient, session pool and healthchecker should be in consistent state.
|
||||
func TestStressSessionPool(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Use concurrent workers to test different session pool built from different configurations.
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
for ti, cfg := range []SessionPoolConfig{
|
||||
SessionPoolConfig{},
|
||||
SessionPoolConfig{MaxSessionAge: 20 * time.Millisecond},
|
||||
SessionPoolConfig{maxSessionAge: 20 * time.Millisecond},
|
||||
SessionPoolConfig{MinOpened: 10, MaxOpened: 100},
|
||||
SessionPoolConfig{MaxBurst: 50},
|
||||
SessionPoolConfig{MaxSessionAge: 20 * time.Millisecond, MinOpened: 10, MaxOpened: 200, MaxBurst: 5},
|
||||
SessionPoolConfig{MaxSessionAge: 20 * time.Millisecond, MinOpened: 10, MaxOpened: 200, MaxBurst: 5, WriteSessions: 0.2},
|
||||
SessionPoolConfig{maxSessionAge: 20 * time.Millisecond, MinOpened: 10, MaxOpened: 200, MaxBurst: 5},
|
||||
SessionPoolConfig{maxSessionAge: 20 * time.Millisecond, MinOpened: 10, MaxOpened: 200, MaxBurst: 5, WriteSessions: 0.2},
|
||||
} {
|
||||
var wg sync.WaitGroup
|
||||
// Create a more aggressive session healthchecker to increase test concurrency.
|
||||
cfg.HealthCheckInterval = 50 * time.Millisecond
|
||||
cfg.healthCheckSampleInterval = 10 * time.Millisecond
|
||||
cfg.HealthCheckWorkers = 50
|
||||
sc := testutil.NewMockCloudSpannerClient(t)
|
||||
sc.MakeNice()
|
||||
@@ -728,9 +771,9 @@ func TestStressSessionPool(t *testing.T) {
|
||||
if takeWrite && sh.getTransactionID() == nil {
|
||||
t.Errorf("%v.%v: pool.takeWriteSession returns session %v without transaction", ti, idx, sh.session)
|
||||
}
|
||||
if int64(cfg.MaxSessionAge) > 0 && rand.Intn(100) < idx {
|
||||
if int64(cfg.maxSessionAge) > 0 && rand.Intn(100) < idx {
|
||||
// Random sleep before destroying/recycling the session, to give healthcheck worker a chance to step in.
|
||||
<-time.After(time.Duration(rand.Int63n(int64(cfg.MaxSessionAge))))
|
||||
<-time.After(time.Duration(rand.Int63n(int64(cfg.maxSessionAge))))
|
||||
}
|
||||
if rand.Intn(100) < idx {
|
||||
// destroy the session.
|
||||
@@ -763,6 +806,7 @@ func TestStressSessionPool(t *testing.T) {
|
||||
}
|
||||
idleSessions[s.getID()] = true
|
||||
}
|
||||
sp.mu.Lock()
|
||||
if int(sp.numOpened) != len(idleSessions) {
|
||||
t.Errorf("%v: number of opened sessions (%v) != number of idle sessions (%v)", ti, sp.numOpened, len(idleSessions))
|
||||
}
|
||||
@@ -776,6 +820,8 @@ func TestStressSessionPool(t *testing.T) {
|
||||
}
|
||||
hcSessions[s.getID()] = true
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
// Verify that idleSessions == hcSessions == mockSessions.
|
||||
if !reflect.DeepEqual(idleSessions, hcSessions) {
|
||||
t.Errorf("%v: sessions in idle list (%v) != sessions in healthcheck queue (%v)", ti, idleSessions, hcSessions)
|
||||
@@ -790,3 +836,66 @@ func TestStressSessionPool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaintainer checks the session pool maintainer maintains the number of sessions in the following cases
|
||||
// 1. On initialization of session pool, replenish session pool to meet MinOpened or MaxIdle.
|
||||
// 2. On increased session usage, provision extra MaxIdle sessions.
|
||||
// 3. After the surge passes, scale down the session pool accordingly.
|
||||
func TestMaintainer(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
var (
|
||||
minOpened uint64 = 5
|
||||
maxIdle uint64 = 4
|
||||
)
|
||||
sp, _, cancel := setup(t, SessionPoolConfig{MinOpened: minOpened, MaxIdle: maxIdle})
|
||||
sampleInterval := sp.SessionPoolConfig.healthCheckSampleInterval
|
||||
hcInterval := sp.SessionPoolConfig.HealthCheckInterval
|
||||
defer cancel()
|
||||
|
||||
<-time.After(sampleInterval * 1)
|
||||
sp.mu.Lock()
|
||||
if sp.numOpened != 5 {
|
||||
t.Errorf("Replenish. Expect %d open, got %d", sp.MinOpened, sp.numOpened)
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
// To save test time, we are not creating many sessions, because the time to create sessions will have impact on the decision on sessionsToKeep. We also parallelize the take and recycle process.
|
||||
shs := make([]*sessionHandle, 10)
|
||||
for i := 0; i < len(shs); i++ {
|
||||
var err error
|
||||
shs[i], err = sp.take(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("cannot get session from session pool: %v", err)
|
||||
}
|
||||
}
|
||||
sp.mu.Lock()
|
||||
if sp.numOpened != 10 {
|
||||
t.Errorf("Scale out from normal use. Expect %d open, got %d", 10, sp.numOpened)
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
<-time.After(sampleInterval)
|
||||
for _, sh := range shs[:7] {
|
||||
sh.recycle()
|
||||
}
|
||||
|
||||
<-time.After(sampleInterval * 2)
|
||||
sp.mu.Lock()
|
||||
if sp.numOpened != 7 {
|
||||
t.Errorf("Keep extra MaxIdle sessions. Expect %d open, got %d", 7, sp.numOpened)
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
for _, sh := range shs[7:] {
|
||||
sh.recycle()
|
||||
}
|
||||
<-time.After(sampleInterval*10 + hcInterval)
|
||||
sp.mu.Lock()
|
||||
if sp.numOpened != minOpened {
|
||||
t.Errorf("Scale down. Expect %d open, got %d", minOpened, sp.numOpened)
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
}
|
||||
|
||||
+261
-123
@@ -17,8 +17,12 @@ limitations under the License.
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -43,14 +47,8 @@ var (
|
||||
// testInstanceID specifies the Cloud Spanner instance used for testing.
|
||||
testInstanceID = "go-integration-test"
|
||||
|
||||
// client is a spanner.Client.
|
||||
client *Client
|
||||
// admin is a spanner.DatabaseAdminClient.
|
||||
admin *database.DatabaseAdminClient
|
||||
// db is the path of the testing database.
|
||||
db string
|
||||
// dbName is the short name of the testing database.
|
||||
dbName string
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -99,29 +97,53 @@ var (
|
||||
|
||||
type testTableRow struct{ Key, StringValue string }
|
||||
|
||||
// prepare initializes Cloud Spanner testing DB and clients.
|
||||
func prepare(ctx context.Context, t *testing.T, statements []string) error {
|
||||
func TestMain(m *testing.M) {
|
||||
initIntegrationTest()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func initIntegrationTest() {
|
||||
flag.Parse() // needed for testing.Short()
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
return
|
||||
}
|
||||
if testProjectID == "" {
|
||||
t.Skip("Integration tests skipped: GCLOUD_TESTS_GOLANG_PROJECT_ID is missing")
|
||||
log.Print("Integration tests skipped: GCLOUD_TESTS_GOLANG_PROJECT_ID is missing")
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
ts := testutil.TokenSource(ctx, AdminScope, Scope)
|
||||
if ts == nil {
|
||||
t.Skip("Integration test skipped: cannot get service account credential from environment variable %v", "GCLOUD_TESTS_GOLANG_KEY")
|
||||
log.Print("Integration test skipped: cannot get service account credential from environment variable %v", "GCLOUD_TESTS_GOLANG_KEY")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
// Create Admin client and Data client.
|
||||
// TODO: Remove the EndPoint option once this is the default.
|
||||
admin, err = database.NewDatabaseAdminClient(ctx, option.WithTokenSource(ts), option.WithEndpoint("spanner.googleapis.com:443"))
|
||||
if err != nil {
|
||||
t.Errorf("cannot create admin client: %v", err)
|
||||
return err
|
||||
log.Fatalf("cannot create admin client: %v", err)
|
||||
}
|
||||
// Construct test DB name.
|
||||
dbName = fmt.Sprintf("gotest_%v", time.Now().UnixNano())
|
||||
db = fmt.Sprintf("projects/%v/instances/%v/databases/%v", testProjectID, testInstanceID, dbName)
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
count int
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
// prepare initializes Cloud Spanner testing DB and clients.
|
||||
func prepare(ctx context.Context, t *testing.T, statements []string) (client *Client, dbPath string, tearDown func()) {
|
||||
if admin == nil {
|
||||
t.Skip("Integration tests skipped")
|
||||
}
|
||||
// Construct a unique test DB name.
|
||||
mu.Lock()
|
||||
dbName := fmt.Sprintf("gotest_%d_%d", now.UnixNano(), count)
|
||||
count++
|
||||
mu.Unlock()
|
||||
|
||||
dbPath = fmt.Sprintf("projects/%v/instances/%v/databases/%v", testProjectID, testInstanceID, dbName)
|
||||
// Create database and tables.
|
||||
op, err := admin.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
|
||||
Parent: fmt.Sprintf("projects/%v/instances/%v", testProjectID, testInstanceID),
|
||||
@@ -129,53 +151,33 @@ func prepare(ctx context.Context, t *testing.T, statements []string) error {
|
||||
ExtraStatements: statements,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("cannot create testing DB %v: %v", db, err)
|
||||
return err
|
||||
t.Fatalf("cannot create testing DB %v: %v", dbPath, err)
|
||||
}
|
||||
if _, err := op.Wait(ctx); err != nil {
|
||||
t.Errorf("cannot create testing DB %v: %v", db, err)
|
||||
return err
|
||||
t.Fatalf("cannot create testing DB %v: %v", dbPath, err)
|
||||
}
|
||||
client, err = NewClientWithConfig(ctx, db, ClientConfig{
|
||||
SessionPoolConfig: SessionPoolConfig{
|
||||
WriteSessions: 0.2,
|
||||
},
|
||||
}, option.WithTokenSource(ts))
|
||||
client, err = NewClientWithConfig(ctx, dbPath, ClientConfig{
|
||||
SessionPoolConfig: SessionPoolConfig{WriteSessions: 0.2},
|
||||
}, option.WithTokenSource(testutil.TokenSource(ctx, Scope)))
|
||||
if err != nil {
|
||||
t.Errorf("cannot create data client on DB %v: %v", db, err)
|
||||
return err
|
||||
t.Fatalf("cannot create data client on DB %v: %v", dbPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tearDown tears down the testing environment created by prepare().
|
||||
func tearDown(ctx context.Context, t *testing.T) {
|
||||
if admin != nil {
|
||||
if err := admin.DropDatabase(ctx, &adminpb.DropDatabaseRequest{db}); err != nil {
|
||||
t.Logf("failed to drop testing database: %v, might need a manual removal", db)
|
||||
return client, dbPath, func() {
|
||||
if err := admin.DropDatabase(ctx, &adminpb.DropDatabaseRequest{dbPath}); err != nil {
|
||||
t.Logf("failed to drop testing database: %v, might need a manual removal", dbPath)
|
||||
}
|
||||
admin.Close()
|
||||
}
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
admin = nil
|
||||
client = nil
|
||||
db = ""
|
||||
}
|
||||
|
||||
// Test SingleUse transaction.
|
||||
func TestSingleUse(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
// Set up testing environment.
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
// If prepare() fails, tear down whatever that's already up.
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
// After all tests, tear down testing environment.
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
writes := []struct {
|
||||
row []interface{}
|
||||
@@ -246,7 +248,7 @@ func TestSingleUse(t *testing.T) {
|
||||
ReadTimestamp(writes[2].ts),
|
||||
func(ts time.Time) error {
|
||||
if ts != writes[2].ts {
|
||||
return fmt.Errorf("read got timestamp %v, expect %v", ts, writes[2].ts)
|
||||
return fmt.Errorf("read got timestamp %v, want %v", ts, writes[2].ts)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -368,16 +370,12 @@ func TestSingleUse(t *testing.T) {
|
||||
// Test ReadOnlyTransaction. The testsuite is mostly like SingleUse, except it
|
||||
// also tests for a single timestamp across multiple reads.
|
||||
func TestReadOnlyTransaction(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
// Set up testing environment.
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
// If prepare() fails, tear down whatever that's already up.
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
// After all tests, tear down testing environment.
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
writes := []struct {
|
||||
row []interface{}
|
||||
@@ -554,16 +552,45 @@ func TestReadOnlyTransaction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test ReadOnlyTransaction with different timestamp bound when there's an update at the same time.
|
||||
func TestUpdateDuringRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
for i, tb := range []TimestampBound{
|
||||
StrongRead(),
|
||||
ReadTimestamp(time.Now().Add(-time.Minute * 30)), // version GC is 1 hour
|
||||
ExactStaleness(time.Minute * 30),
|
||||
} {
|
||||
ro := client.ReadOnlyTransaction().WithTimestampBound(tb)
|
||||
_, err := ro.ReadRow(ctx, "Singers", Key{i}, []string{"SingerId"})
|
||||
if ErrCode(err) != codes.NotFound {
|
||||
t.Errorf("%d: ReadOnlyTransaction.ReadRow before write returns error: %v, want NotFound", i, err)
|
||||
}
|
||||
|
||||
m := InsertOrUpdate("Singers", []string{"SingerId"}, []interface{}{i})
|
||||
if _, err := client.Apply(ctx, []*Mutation{m}, ApplyAtLeastOnce()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = ro.ReadRow(ctx, "Singers", Key{i}, []string{"SingerId"})
|
||||
if ErrCode(err) != codes.NotFound {
|
||||
t.Errorf("%d: ReadOnlyTransaction.ReadRow after write returns error: %v, want NotFound", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test ReadWriteTransaction.
|
||||
func TestReadWriteTransaction(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Give a longer deadline because of transaction backoffs.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
// Set up two accounts
|
||||
accounts := []*Mutation{
|
||||
@@ -656,16 +683,12 @@ const (
|
||||
var testTableColumns = []string{"Key", "StringValue"}
|
||||
|
||||
func TestReads(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
// Set up testing environment.
|
||||
if err := prepare(ctx, t, readDBStatements); err != nil {
|
||||
// If prepare() fails, tear down whatever that's already up.
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
// After all tests, tear down testing environment.
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, readDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
// Includes k0..k14. Strings sort lexically, eg "k1" < "k10" < "k2".
|
||||
var ms []*Mutation
|
||||
@@ -720,11 +743,11 @@ func TestReads(t *testing.T) {
|
||||
|
||||
// No index point read not found, because Go does not have ReadRowUsingIndex.
|
||||
|
||||
rangeReads(ctx, t)
|
||||
indexRangeReads(ctx, t)
|
||||
rangeReads(ctx, t, client)
|
||||
indexRangeReads(ctx, t, client)
|
||||
}
|
||||
|
||||
func rangeReads(ctx context.Context, t *testing.T) {
|
||||
func rangeReads(ctx context.Context, t *testing.T, client *Client) {
|
||||
checkRange := func(ks KeySet, wantNums ...int) {
|
||||
if msg, ok := compareRows(client.Single().Read(ctx, testTable, ks, testTableColumns), wantNums); !ok {
|
||||
t.Errorf("key set %+v: %s", ks, msg)
|
||||
@@ -757,7 +780,7 @@ func rangeReads(ctx context.Context, t *testing.T) {
|
||||
checkRange(AllKeys(), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
|
||||
}
|
||||
|
||||
func indexRangeReads(ctx context.Context, t *testing.T) {
|
||||
func indexRangeReads(ctx context.Context, t *testing.T, client *Client) {
|
||||
checkRange := func(ks KeySet, wantNums ...int) {
|
||||
if msg, ok := compareRows(client.Single().ReadUsingIndex(ctx, testTable, testTableIndex, ks, testTableColumns),
|
||||
wantNums); !ok {
|
||||
@@ -816,18 +839,14 @@ func compareRows(iter *RowIterator, wantNums []int) (string, bool) {
|
||||
}
|
||||
|
||||
func TestEarlyTimestamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test that we can get the timestamp from a read-only transaction as
|
||||
// soon as we have read at least one row.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
// Set up testing environment.
|
||||
if err := prepare(ctx, t, readDBStatements); err != nil {
|
||||
// If prepare() fails, tear down whatever that's already up.
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
// After all tests, tear down testing environment.
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, readDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
var ms []*Mutation
|
||||
for i := 0; i < 3; i++ {
|
||||
@@ -867,13 +886,11 @@ func TestEarlyTimestamp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNestedTransaction(t *testing.T) {
|
||||
t.Parallel()
|
||||
// You cannot use a transaction from inside a read-write transaction.
|
||||
ctx := context.Background()
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *ReadWriteTransaction) error {
|
||||
_, err := client.ReadWriteTransaction(ctx,
|
||||
func(context.Context, *ReadWriteTransaction) error { return nil })
|
||||
@@ -896,17 +913,15 @@ func TestNestedTransaction(t *testing.T) {
|
||||
|
||||
// Test client recovery on database recreation.
|
||||
func TestDbRemovalRecovery(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, dbPath, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
// Drop the testing database.
|
||||
if err := admin.DropDatabase(ctx, &adminpb.DropDatabaseRequest{db}); err != nil {
|
||||
t.Fatalf("failed to drop testing database %v: %v", db, err)
|
||||
if err := admin.DropDatabase(ctx, &adminpb.DropDatabaseRequest{dbPath}); err != nil {
|
||||
t.Fatalf("failed to drop testing database %v: %v", dbPath, err)
|
||||
}
|
||||
|
||||
// Now, send the query.
|
||||
@@ -917,6 +932,7 @@ func TestDbRemovalRecovery(t *testing.T) {
|
||||
}
|
||||
|
||||
// Recreate database and table.
|
||||
dbName := dbPath[strings.LastIndex(dbPath, "/")+1:]
|
||||
op, err := admin.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
|
||||
Parent: fmt.Sprintf("projects/%v/instances/%v", testProjectID, testInstanceID),
|
||||
CreateStatement: "CREATE DATABASE " + dbName,
|
||||
@@ -930,7 +946,7 @@ func TestDbRemovalRecovery(t *testing.T) {
|
||||
},
|
||||
})
|
||||
if _, err := op.Wait(ctx); err != nil {
|
||||
t.Errorf("cannot recreate testing DB %v: %v", db, err)
|
||||
t.Errorf("cannot recreate testing DB %v: %v", dbPath, err)
|
||||
}
|
||||
|
||||
// Now, send the query again.
|
||||
@@ -938,19 +954,17 @@ func TestDbRemovalRecovery(t *testing.T) {
|
||||
defer iter.Stop()
|
||||
_, err = iter.Next()
|
||||
if err != nil && err != iterator.Done {
|
||||
t.Fatalf("failed to send query to database %v: %v", db, err)
|
||||
t.Fatalf("failed to send query to database %v: %v", dbPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test encoding/decoding non-struct Cloud Spanner types.
|
||||
func TestBasicTypes(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
t1, _ := time.Parse(time.RFC3339Nano, "2016-11-15T15:04:05.999999999Z")
|
||||
// Boundaries
|
||||
t2, _ := time.Parse(time.RFC3339Nano, "0001-01-01T00:00:00.000000000Z")
|
||||
@@ -1087,13 +1101,11 @@ func TestBasicTypes(t *testing.T) {
|
||||
|
||||
// Test decoding Cloud Spanner STRUCT type.
|
||||
func TestStructTypes(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
if err := prepare(ctx, t, singerDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
tests := []struct {
|
||||
q Statement
|
||||
@@ -1176,12 +1188,10 @@ func TestStructTypes(t *testing.T) {
|
||||
|
||||
// Test queries of the form "SELECT expr".
|
||||
func TestQueryExpressions(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
if err := prepare(ctx, t, nil); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, nil)
|
||||
defer tearDown()
|
||||
|
||||
newRow := func(vals []interface{}) *Row {
|
||||
row, err := NewRow(make([]string, len(vals)), vals)
|
||||
@@ -1240,6 +1250,7 @@ func isNaN(x interface{}) bool {
|
||||
}
|
||||
|
||||
func TestInvalidDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
}
|
||||
@@ -1264,14 +1275,12 @@ func TestInvalidDatabase(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReadErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
if err := prepare(ctx, t, readDBStatements); err != nil {
|
||||
tearDown(ctx, t)
|
||||
t.Fatalf("cannot set up testing environment: %v", err)
|
||||
}
|
||||
defer tearDown(ctx, t)
|
||||
client, _, tearDown := prepare(ctx, t, readDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
// Read over invalid table fails
|
||||
// Read over invalid table fails
|
||||
_, err := client.Single().ReadRow(ctx, "badTable", Key{1}, []string{"StringValue"})
|
||||
if msg, ok := matchError(err, codes.NotFound, "badTable"); !ok {
|
||||
t.Error(msg)
|
||||
@@ -1282,7 +1291,7 @@ func TestReadErrors(t *testing.T) {
|
||||
t.Error(msg)
|
||||
}
|
||||
|
||||
// Invalid query fails
|
||||
// Invalid query fails
|
||||
iter := client.Single().Query(ctx, Statement{SQL: "SELECT Apples AND Oranges"})
|
||||
defer iter.Stop()
|
||||
_, err = iter.Next()
|
||||
@@ -1297,7 +1306,7 @@ func TestReadErrors(t *testing.T) {
|
||||
if msg, ok := matchError(err, codes.Canceled, ""); !ok {
|
||||
t.Error(msg)
|
||||
}
|
||||
// Read should fail if deadline exceeded.
|
||||
// Read should fail if deadline exceeded.
|
||||
dctx, _ := context.WithTimeout(ctx, time.Nanosecond)
|
||||
<-dctx.Done()
|
||||
_, err = client.Single().ReadRow(dctx, "TestTable", Key{1}, []string{"StringValue"})
|
||||
@@ -1365,3 +1374,132 @@ func readAllTestTable(iter *RowIterator) ([]testTableRow, error) {
|
||||
vals = append(vals, ttr)
|
||||
}
|
||||
}
|
||||
|
||||
// Test TransactionRunner. Test that transactions are aborted and retried as expected.
|
||||
func TestTransactionRunner(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
client, _, tearDown := prepare(ctx, t, singerDBStatements)
|
||||
defer tearDown()
|
||||
|
||||
// Test 1: User error should abort the transaction.
|
||||
_, _ = client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *ReadWriteTransaction) error {
|
||||
tx.BufferWrite([]*Mutation{
|
||||
Insert("Accounts", []string{"AccountId", "Nickname", "Balance"}, []interface{}{int64(1), "Foo", int64(50)})})
|
||||
return errors.New("user error")
|
||||
})
|
||||
// Empty read.
|
||||
rows, err := readAllTestTable(client.Single().Read(ctx, "Accounts", Key{1}, []string{"AccountId", "Nickname", "Balance"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := len(rows), 0; got != want {
|
||||
t.Errorf("Empty read, got %d, want %d.", got, want)
|
||||
}
|
||||
|
||||
// Test 2: Expect abort and retry.
|
||||
// We run two ReadWriteTransactions concurrently and make txn1 abort txn2 by committing writes to the column txn2 have read,
|
||||
// and expect the following read to abort and txn2 retries.
|
||||
|
||||
// Set up two accounts
|
||||
accounts := []*Mutation{
|
||||
Insert("Accounts", []string{"AccountId", "Balance"}, []interface{}{int64(1), int64(0)}),
|
||||
Insert("Accounts", []string{"AccountId", "Balance"}, []interface{}{int64(2), int64(1)}),
|
||||
}
|
||||
if _, err := client.Apply(ctx, accounts, ApplyAtLeastOnce()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var (
|
||||
cTxn1Start = make(chan struct{})
|
||||
cTxn1Commit = make(chan struct{})
|
||||
cTxn2Start = make(chan struct{})
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
// read balance, check error if we don't expect abort.
|
||||
readBalance := func(tx interface {
|
||||
ReadRow(ctx context.Context, table string, key Key, columns []string) (*Row, error)
|
||||
}, key int64, expectAbort bool) (int64, error) {
|
||||
var b int64
|
||||
r, e := tx.ReadRow(ctx, "Accounts", Key{int64(key)}, []string{"Balance"})
|
||||
if e != nil {
|
||||
if expectAbort && !isAbortErr(e) {
|
||||
t.Errorf("ReadRow got %v, want Abort error.", e)
|
||||
}
|
||||
return b, e
|
||||
}
|
||||
if ce := r.Column(0, &b); ce != nil {
|
||||
return b, ce
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
wg.Add(2)
|
||||
// Txn 1
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var once sync.Once
|
||||
_, e := client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *ReadWriteTransaction) error {
|
||||
b, e := readBalance(tx, 1, false)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
// txn 1 can abort, in that case we skip closing the channel on retry.
|
||||
once.Do(func() { close(cTxn1Start) })
|
||||
tx.BufferWrite([]*Mutation{
|
||||
Update("Accounts", []string{"AccountId", "Balance"}, []interface{}{int64(1), int64(b + 1)})})
|
||||
// Wait for second transaction.
|
||||
<-cTxn2Start
|
||||
return nil
|
||||
})
|
||||
close(cTxn1Commit)
|
||||
if e != nil {
|
||||
t.Errorf("Transaction 1 commit, got %v, want nil.", e)
|
||||
}
|
||||
}()
|
||||
// Txn 2
|
||||
go func() {
|
||||
// Wait until txn 1 starts.
|
||||
<-cTxn1Start
|
||||
defer wg.Done()
|
||||
var (
|
||||
once sync.Once
|
||||
b1 int64
|
||||
b2 int64
|
||||
e error
|
||||
)
|
||||
_, e = client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *ReadWriteTransaction) error {
|
||||
if b1, e = readBalance(tx, 1, false); e != nil {
|
||||
return e
|
||||
}
|
||||
// Skip closing channel on retry.
|
||||
once.Do(func() { close(cTxn2Start) })
|
||||
// Wait until txn 1 successfully commits.
|
||||
<-cTxn1Commit
|
||||
// Txn1 has committed and written a balance to the account.
|
||||
// Now this transaction (txn2) reads and re-writes the balance.
|
||||
// The first time through, it will abort because it overlaps with txn1.
|
||||
// Then it will retry after txn1 commits, and succeed.
|
||||
if b2, e = readBalance(tx, 2, true); e != nil {
|
||||
return e
|
||||
}
|
||||
tx.BufferWrite([]*Mutation{
|
||||
Update("Accounts", []string{"AccountId", "Balance"}, []interface{}{int64(2), int64(b1 + b2)})})
|
||||
return nil
|
||||
})
|
||||
if e != nil {
|
||||
t.Errorf("Transaction 2 commit, got %v, want nil.", e)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
// Check that both transactions' effects are visible.
|
||||
for i := int64(1); i <= int64(2); i++ {
|
||||
if b, e := readBalance(client.Single(), i, false); e != nil {
|
||||
t.Fatalf("ReadBalance for key %d error %v.", i, e)
|
||||
} else if b != i {
|
||||
t.Errorf("Balance for key %d, got %d, want %d.", i, b, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
-2
@@ -17,8 +17,12 @@ limitations under the License.
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/civil"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
proto3 "github.com/golang/protobuf/ptypes/struct"
|
||||
@@ -39,22 +43,101 @@ func TestBindParams(t *testing.T) {
|
||||
},
|
||||
ParamTypes: map[string]*sppb.Type{"var": nil},
|
||||
}
|
||||
var (
|
||||
t1, _ = time.Parse(time.RFC3339Nano, "2016-11-15T15:04:05.999999999Z")
|
||||
// Boundaries
|
||||
t2, _ = time.Parse(time.RFC3339Nano, "0001-01-01T00:00:00.000000000Z")
|
||||
t3, _ = time.Parse(time.RFC3339Nano, "9999-12-31T23:59:59.999999999Z")
|
||||
d1, _ = civil.ParseDate("2016-11-15")
|
||||
// Boundaries
|
||||
d2, _ = civil.ParseDate("0001-01-01")
|
||||
d3, _ = civil.ParseDate("9999-12-31")
|
||||
)
|
||||
for i, test := range []struct {
|
||||
val interface{}
|
||||
wantField *proto3.Value
|
||||
wantType *sppb.Type
|
||||
}{
|
||||
{"abc", stringProto("abc"), stringType()},
|
||||
{int64(1), intProto(1), intType()},
|
||||
// bool
|
||||
{true, boolProto(true), boolType()},
|
||||
{NullBool{true, true}, boolProto(true), boolType()},
|
||||
{NullBool{true, false}, nullProto(), boolType()},
|
||||
{[]bool(nil), nullProto(), listType(boolType())},
|
||||
{[]bool{}, listProto(), listType(boolType())},
|
||||
{[]bool{true, false}, listProto(boolProto(true), boolProto(false)), listType(boolType())},
|
||||
{[]NullBool(nil), nullProto(), listType(boolType())},
|
||||
{[]NullBool{}, listProto(), listType(boolType())},
|
||||
{[]NullBool{{true, true}, {}}, listProto(boolProto(true), nullProto()), listType(boolType())},
|
||||
// int
|
||||
{int(1), intProto(1), intType()},
|
||||
{[]int(nil), nullProto(), listType(intType())},
|
||||
{[]int{}, listProto(), listType(intType())},
|
||||
{[]int{1, 2}, listProto(intProto(1), intProto(2)), listType(intType())},
|
||||
// int64
|
||||
{int64(1), intProto(1), intType()},
|
||||
{NullInt64{5, true}, intProto(5), intType()},
|
||||
{NullInt64{5, false}, nullProto(), intType()},
|
||||
{[]int64(nil), nullProto(), listType(intType())},
|
||||
{[]int64{}, listProto(), listType(intType())},
|
||||
{[]int64{1, 2}, listProto(intProto(1), intProto(2)), listType(intType())},
|
||||
{[]NullInt64(nil), nullProto(), listType(intType())},
|
||||
{[]NullInt64{}, listProto(), listType(intType())},
|
||||
{[]NullInt64{{1, true}, {}}, listProto(intProto(1), nullProto()), listType(intType())},
|
||||
// float64
|
||||
{0.0, floatProto(0.0), floatType()},
|
||||
{math.Inf(1), floatProto(math.Inf(1)), floatType()},
|
||||
{math.Inf(-1), floatProto(math.Inf(-1)), floatType()},
|
||||
{math.NaN(), floatProto(math.NaN()), floatType()},
|
||||
{NullFloat64{2.71, true}, floatProto(2.71), floatType()},
|
||||
{NullFloat64{1.41, false}, nullProto(), floatType()},
|
||||
{[]float64(nil), nullProto(), listType(floatType())},
|
||||
{[]float64{}, listProto(), listType(floatType())},
|
||||
{[]float64{2.72, math.Inf(1)}, listProto(floatProto(2.72), floatProto(math.Inf(1))), listType(floatType())},
|
||||
{[]NullFloat64(nil), nullProto(), listType(floatType())},
|
||||
{[]NullFloat64{}, listProto(), listType(floatType())},
|
||||
{[]NullFloat64{{2.72, true}, {}}, listProto(floatProto(2.72), nullProto()), listType(floatType())},
|
||||
// string
|
||||
{"", stringProto(""), stringType()},
|
||||
{"foo", stringProto("foo"), stringType()},
|
||||
{NullString{"bar", true}, stringProto("bar"), stringType()},
|
||||
{NullString{"bar", false}, nullProto(), stringType()},
|
||||
{[]string(nil), nullProto(), listType(stringType())},
|
||||
{[]string{}, listProto(), listType(stringType())},
|
||||
{[]string{"foo", "bar"}, listProto(stringProto("foo"), stringProto("bar")), listType(stringType())},
|
||||
{[]NullString(nil), nullProto(), listType(stringType())},
|
||||
{[]NullString{}, listProto(), listType(stringType())},
|
||||
{[]NullString{{"foo", true}, {}}, listProto(stringProto("foo"), nullProto()), listType(stringType())},
|
||||
// bytes
|
||||
{[]byte{}, bytesProto([]byte{}), bytesType()},
|
||||
{[]byte{1, 2, 3}, bytesProto([]byte{1, 2, 3}), bytesType()},
|
||||
{[]byte(nil), nullProto(), bytesType()},
|
||||
{[][]byte(nil), nullProto(), listType(bytesType())},
|
||||
{[][]byte{}, listProto(), listType(bytesType())},
|
||||
{[][]byte{[]byte{1}, []byte(nil)}, listProto(bytesProto([]byte{1}), nullProto()), listType(bytesType())},
|
||||
// date
|
||||
{d1, dateProto(d1), dateType()},
|
||||
{NullDate{civil.Date{}, false}, nullProto(), dateType()},
|
||||
{[]civil.Date(nil), nullProto(), listType(dateType())},
|
||||
{[]civil.Date{}, listProto(), listType(dateType())},
|
||||
{[]civil.Date{d1, d2, d3}, listProto(dateProto(d1), dateProto(d2), dateProto(d3)), listType(dateType())},
|
||||
{[]NullDate{NullDate{d2, true}, NullDate{}}, listProto(dateProto(d2), nullProto()), listType(dateType())},
|
||||
// timestamp
|
||||
{t1, timeProto(t1), timeType()},
|
||||
{NullTime{}, nullProto(), timeType()},
|
||||
{[]time.Time(nil), nullProto(), listType(timeType())},
|
||||
{[]time.Time{}, listProto(), listType(timeType())},
|
||||
{[]time.Time{t1, t2, t3}, listProto(timeProto(t1), timeProto(t2), timeProto(t3)), listType(timeType())},
|
||||
{[]NullTime{NullTime{t2, true}, NullTime{}}, listProto(timeProto(t2), nullProto()), listType(timeType())},
|
||||
} {
|
||||
st.Params["var"] = test.val
|
||||
want.Params.Fields["var"] = test.wantField
|
||||
want.ParamTypes["var"] = test.wantType
|
||||
got := &sppb.ExecuteSqlRequest{}
|
||||
if err := st.bindParams(got); err != nil || !proto.Equal(got, want) {
|
||||
// handle NaN
|
||||
if test.wantType.Code == floatType().Code && proto.MarshalTextString(got) == proto.MarshalTextString(want) {
|
||||
continue
|
||||
}
|
||||
t.Errorf("#%d: bind result: \n(%v, %v)\nwant\n(%v, %v)\n", i, got, err, want, nil)
|
||||
}
|
||||
}
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/spanner/internal/testutil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
sppb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
errAbrt = spannerErrorf(codes.Aborted, "")
|
||||
errUsr = errors.New("error")
|
||||
)
|
||||
|
||||
// setup sets up a Client using mockclient
|
||||
func mockClient(t *testing.T) (*sessionPool, *testutil.MockCloudSpannerClient, *Client) {
|
||||
var (
|
||||
mc = testutil.NewMockCloudSpannerClient(t)
|
||||
spc = SessionPoolConfig{}
|
||||
database = "mockdb"
|
||||
)
|
||||
spc.getRPCClient = func() (sppb.SpannerClient, error) {
|
||||
return mc, nil
|
||||
}
|
||||
sp, err := newSessionPool(database, spc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create session pool: %v", err)
|
||||
}
|
||||
return sp, mc, &Client{
|
||||
database: database,
|
||||
idleSessions: sp,
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadOnlyAcquire tests acquire for ReadOnlyTransaction.
|
||||
func TestReadOnlyAcquire(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, mc, client := mockClient(t)
|
||||
defer client.Close()
|
||||
acts := []testutil.Action{
|
||||
testutil.NewAction("Begin", errUsr),
|
||||
testutil.NewAction("Begin", nil),
|
||||
testutil.NewAction("Begin", nil),
|
||||
}
|
||||
mc.SetActions(acts...)
|
||||
// Singleuse should only be used once.
|
||||
txn := client.Single()
|
||||
defer txn.Close()
|
||||
_, _, e := txn.acquire(context.Background())
|
||||
if e != nil {
|
||||
t.Errorf("Acquire for single use, got %v, want nil.", e)
|
||||
}
|
||||
_, _, e = txn.acquire(context.Background())
|
||||
if wantErr := errTxClosed(); !reflect.DeepEqual(e, wantErr) {
|
||||
t.Errorf("Second acquire for single use, got %v, want %v.", e, wantErr)
|
||||
}
|
||||
// Multiuse can recover from acquire failure.
|
||||
txn = client.ReadOnlyTransaction()
|
||||
_, _, e = txn.acquire(context.Background())
|
||||
if wantErr := toSpannerError(errUsr); !reflect.DeepEqual(e, wantErr) {
|
||||
t.Errorf("Acquire for multi use, got %v, want %v.", e, wantErr)
|
||||
}
|
||||
_, _, e = txn.acquire(context.Background())
|
||||
if e != nil {
|
||||
t.Errorf("Acquire for multi use, got %v, want nil.", e)
|
||||
}
|
||||
txn.Close()
|
||||
// Multiuse can not be used after close.
|
||||
_, _, e = txn.acquire(context.Background())
|
||||
if wantErr := errTxClosed(); !reflect.DeepEqual(e, wantErr) {
|
||||
t.Errorf("Second acquire for multi use, got %v, want %v.", e, wantErr)
|
||||
}
|
||||
// Multiuse can be acquired concurrently.
|
||||
txn = client.ReadOnlyTransaction()
|
||||
defer txn.Close()
|
||||
mc.Freeze()
|
||||
var (
|
||||
sh1 *sessionHandle
|
||||
sh2 *sessionHandle
|
||||
ts1 *sppb.TransactionSelector
|
||||
ts2 *sppb.TransactionSelector
|
||||
wg = sync.WaitGroup{}
|
||||
)
|
||||
acquire := func(sh **sessionHandle, ts **sppb.TransactionSelector) {
|
||||
defer wg.Done()
|
||||
var e error
|
||||
*sh, *ts, e = txn.acquire(context.Background())
|
||||
if e != nil {
|
||||
t.Errorf("Concurrent acquire for multiuse, got %v, expect nil.", e)
|
||||
}
|
||||
}
|
||||
wg.Add(2)
|
||||
go acquire(&sh1, &ts1)
|
||||
go acquire(&sh2, &ts2)
|
||||
<-time.After(100 * time.Millisecond)
|
||||
mc.Unfreeze()
|
||||
wg.Wait()
|
||||
if !reflect.DeepEqual(sh1, sh2) {
|
||||
t.Errorf("Expect acquire to get same session handle, got %v and %v.", sh1, sh2)
|
||||
}
|
||||
if !reflect.DeepEqual(ts1, ts2) {
|
||||
t.Errorf("Expect acquire to get same transaction selector, got %v and %v.", ts1, ts2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRetryOnAbort tests transaction retries on abort.
|
||||
func TestRetryOnAbort(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, mc, client := mockClient(t)
|
||||
defer client.Close()
|
||||
// commit in writeOnlyTransaction
|
||||
acts := []testutil.Action{
|
||||
testutil.NewAction("Commit", errAbrt), // abort on first commit
|
||||
testutil.NewAction("Commit", nil),
|
||||
}
|
||||
mc.SetActions(acts...)
|
||||
ms := []*Mutation{
|
||||
Insert("Accounts", []string{"AccountId", "Nickname", "Balance"}, []interface{}{int64(1), "Foo", int64(50)}),
|
||||
Insert("Accounts", []string{"AccountId", "Nickname", "Balance"}, []interface{}{int64(2), "Bar", int64(1)}),
|
||||
}
|
||||
if _, e := client.Apply(context.Background(), ms, ApplyAtLeastOnce()); e != nil {
|
||||
t.Errorf("applyAtLeastOnce retry on abort, got %v, want nil.", e)
|
||||
}
|
||||
// begin and commit in ReadWriteTransaction
|
||||
acts = []testutil.Action{
|
||||
testutil.NewAction("Begin", nil), // let takeWriteSession succeed and get a session handle
|
||||
testutil.NewAction("Commit", errAbrt), // let first commit fail and retry will begin new transaction
|
||||
testutil.NewAction("Begin", errAbrt), // this time we can fail the begin attempt
|
||||
testutil.NewAction("Begin", nil),
|
||||
testutil.NewAction("Commit", nil),
|
||||
}
|
||||
mc.SetActions(acts...)
|
||||
if _, e := client.Apply(context.Background(), ms); e != nil {
|
||||
t.Errorf("ReadWriteTransaction retry on abort, got %v, want nil.", e)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBadSession tests bad session (session not found error).
|
||||
// TODO: session closed from transaction close
|
||||
func TestBadSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
sp, mc, client := mockClient(t)
|
||||
defer client.Close()
|
||||
var sid string
|
||||
// Prepare a session, get the session id for use in testing.
|
||||
if s, e := sp.take(ctx); e != nil {
|
||||
t.Fatal("Prepare session failed.")
|
||||
} else {
|
||||
sid = s.getID()
|
||||
s.recycle()
|
||||
}
|
||||
|
||||
wantErr := spannerErrorf(codes.NotFound, "Session not found: %v", sid)
|
||||
// ReadOnlyTransaction
|
||||
acts := []testutil.Action{
|
||||
testutil.NewAction("Begin", wantErr),
|
||||
testutil.NewAction("Begin", wantErr),
|
||||
testutil.NewAction("Begin", wantErr),
|
||||
}
|
||||
mc.SetActions(acts...)
|
||||
txn := client.ReadOnlyTransaction()
|
||||
defer txn.Close()
|
||||
if _, _, got := txn.acquire(ctx); !reflect.DeepEqual(wantErr, got) {
|
||||
t.Errorf("Expect acquire to fail, got %v, want %v.", got, wantErr)
|
||||
}
|
||||
// The failure should recycle the session, we expect it to be used in following requests.
|
||||
if got := txn.Query(ctx, NewStatement("SELECT 1")); !reflect.DeepEqual(wantErr, got.err) {
|
||||
t.Errorf("Expect Query to fail, got %v, want %v.", got.err, wantErr)
|
||||
}
|
||||
if got := txn.Read(ctx, "Users", KeySets(Key{"alice"}, Key{"bob"}), []string{"name", "email"}); !reflect.DeepEqual(wantErr, got.err) {
|
||||
t.Errorf("Expect Read to fail, got %v, want %v.", got.err, wantErr)
|
||||
}
|
||||
// writeOnlyTransaction
|
||||
ms := []*Mutation{
|
||||
Insert("Accounts", []string{"AccountId", "Nickname", "Balance"}, []interface{}{int64(1), "Foo", int64(50)}),
|
||||
Insert("Accounts", []string{"AccountId", "Nickname", "Balance"}, []interface{}{int64(2), "Bar", int64(1)}),
|
||||
}
|
||||
acts = []testutil.Action{
|
||||
testutil.NewAction("Commit", wantErr),
|
||||
}
|
||||
mc.SetActions(acts...)
|
||||
if _, got := client.Apply(context.Background(), ms, ApplyAtLeastOnce()); !reflect.DeepEqual(wantErr, got) {
|
||||
t.Errorf("Expect applyAtLeastOnce to fail, got %v, want %v.", got, wantErr)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package spanner
|
||||
|
||||
// maxUint64 returns the maximum of two uint64
|
||||
func maxUint64(a, b uint64) uint64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// minUint64 returns the minimum of two uint64
|
||||
func minUint64(a, b uint64) uint64 {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
+255
-57
@@ -128,6 +128,9 @@ type NullRow struct {
|
||||
// column. See google.spanner.v1.ResultSet proto for details. This can be
|
||||
// useful for proxying query results when the result types are not known in
|
||||
// advance.
|
||||
//
|
||||
// If you populate a GenericColumnValue from a row using Row.Column or related
|
||||
// methods, do not modify the contents of Type and Value.
|
||||
type GenericColumnValue struct {
|
||||
Type *sppb.Type
|
||||
Value *proto3.Value
|
||||
@@ -151,12 +154,12 @@ func newGenericColumnValue(v interface{}) (*GenericColumnValue, error) {
|
||||
|
||||
// errTypeMismatch returns error for destination not having a compatible type
|
||||
// with source Cloud Spanner type.
|
||||
func errTypeMismatch(srcType sppb.TypeCode, isArray bool, dst interface{}) error {
|
||||
usage := srcType.String()
|
||||
if isArray {
|
||||
usage = fmt.Sprintf("%v[%v]", sppb.TypeCode_ARRAY, srcType)
|
||||
func errTypeMismatch(srcCode, elCode sppb.TypeCode, dst interface{}) error {
|
||||
s := srcCode.String()
|
||||
if srcCode == sppb.TypeCode_ARRAY {
|
||||
s = fmt.Sprintf("%v[%v]", srcCode, elCode)
|
||||
}
|
||||
return spannerErrorf(codes.InvalidArgument, "type %T cannot be used for decoding %v", dst, usage)
|
||||
return spannerErrorf(codes.InvalidArgument, "type %T cannot be used for decoding %s", dst, s)
|
||||
}
|
||||
|
||||
// errNilSpannerType returns error for nil Cloud Spanner type in decoding.
|
||||
@@ -196,7 +199,7 @@ func parseNullTime(v *proto3.Value, p *NullTime, code sppb.TypeCode, isNull bool
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_TIMESTAMP {
|
||||
return errTypeMismatch(code, false, p)
|
||||
return errTypeMismatch(code, sppb.TypeCode_TYPE_CODE_UNSPECIFIED, p)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullTime{}
|
||||
@@ -232,11 +235,6 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
}
|
||||
acode = t.ArrayElementType.Code
|
||||
}
|
||||
typeErr := errTypeMismatch(code, false, ptr)
|
||||
if code == sppb.TypeCode_ARRAY {
|
||||
typeErr = errTypeMismatch(acode, true, ptr)
|
||||
}
|
||||
nullErr := errDstNotForNull(ptr)
|
||||
_, isNull := v.Kind.(*proto3.Value_NullValue)
|
||||
|
||||
// Do the decoding based on the type of ptr.
|
||||
@@ -248,10 +246,10 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_STRING {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
x, err := getStringValue(v)
|
||||
if err != nil {
|
||||
@@ -263,7 +261,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_STRING {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullString{}
|
||||
@@ -280,7 +278,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_STRING {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeNullStringArray(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]string:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_STRING {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -300,7 +318,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_BYTES {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -320,7 +338,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_BYTES {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -340,10 +358,10 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_INT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
x, err := getStringValue(v)
|
||||
if err != nil {
|
||||
@@ -359,7 +377,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_INT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullInt64{}
|
||||
@@ -380,7 +398,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_INT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -390,7 +408,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeIntArray(x)
|
||||
y, err := decodeNullInt64Array(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]int64:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_INT64 {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeInt64Array(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -400,10 +438,10 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_BOOL {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
x, err := getBoolValue(v)
|
||||
if err != nil {
|
||||
@@ -415,7 +453,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_BOOL {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullBool{}
|
||||
@@ -432,7 +470,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_BOOL {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeNullBoolArray(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]bool:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_BOOL {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -452,10 +510,10 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_FLOAT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
x, err := getFloat64Value(v)
|
||||
if err != nil {
|
||||
@@ -467,7 +525,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_FLOAT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullFloat64{}
|
||||
@@ -484,7 +542,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_FLOAT64 {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeNullFloat64Array(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]float64:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_FLOAT64 {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -502,7 +580,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
case *time.Time:
|
||||
var nt NullTime
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
err := parseNullTime(v, &nt, code, isNull)
|
||||
if err != nil {
|
||||
@@ -519,7 +597,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_TIMESTAMP {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeNullTimeArray(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]time.Time:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_TIMESTAMP {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -539,10 +637,10 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_DATE {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
return nullErr
|
||||
return errDstNotForNull(ptr)
|
||||
}
|
||||
x, err := getStringValue(v)
|
||||
if err != nil {
|
||||
@@ -558,7 +656,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if code != sppb.TypeCode_DATE {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = NullDate{}
|
||||
@@ -579,7 +677,27 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_DATE {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
break
|
||||
}
|
||||
x, err := getListValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y, err := decodeNullDateArray(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = y
|
||||
case *[]civil.Date:
|
||||
if p == nil {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_DATE {
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -599,7 +717,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
return errNilDst(p)
|
||||
}
|
||||
if acode != sppb.TypeCode_STRUCT {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
if isNull {
|
||||
*p = nil
|
||||
@@ -615,16 +733,11 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
}
|
||||
*p = y
|
||||
case *GenericColumnValue:
|
||||
*p = GenericColumnValue{
|
||||
// Deep clone to ensure subsequent changes to t or v
|
||||
// don't affect our decoded value.
|
||||
Type: proto.Clone(t).(*sppb.Type),
|
||||
Value: proto.Clone(v).(*proto3.Value),
|
||||
}
|
||||
*p = GenericColumnValue{Type: t, Value: v}
|
||||
default:
|
||||
// Check if the proto encoding is for an array of structs.
|
||||
if !(code == sppb.TypeCode_ARRAY && acode == sppb.TypeCode_STRUCT) {
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
vp := reflect.ValueOf(p)
|
||||
if !vp.IsValid() {
|
||||
@@ -632,7 +745,7 @@ func decodeValue(v *proto3.Value, t *sppb.Type, ptr interface{}) error {
|
||||
}
|
||||
if !isPtrStructPtrSlice(vp.Type()) {
|
||||
// The container is not a pointer to a struct pointer slice.
|
||||
return typeErr
|
||||
return errTypeMismatch(code, acode, ptr)
|
||||
}
|
||||
// Only use reflection for nil detection on slow path.
|
||||
// Also, IsNil panics on many types, so check it after the type check.
|
||||
@@ -739,8 +852,8 @@ func errDecodeArrayElement(i int, v proto.Message, sqlType string, err error) er
|
||||
return se
|
||||
}
|
||||
|
||||
// decodeStringArray decodes proto3.ListValue pb into a NullString slice.
|
||||
func decodeStringArray(pb *proto3.ListValue) ([]NullString, error) {
|
||||
// decodeNullStringArray decodes proto3.ListValue pb into a NullString slice.
|
||||
func decodeNullStringArray(pb *proto3.ListValue) ([]NullString, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("STRING")
|
||||
}
|
||||
@@ -753,8 +866,23 @@ func decodeStringArray(pb *proto3.ListValue) ([]NullString, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeIntArray decodes proto3.ListValue pb into a NullInt64 slice.
|
||||
func decodeIntArray(pb *proto3.ListValue) ([]NullInt64, error) {
|
||||
// decodeStringArray decodes proto3.ListValue pb into a string slice.
|
||||
func decodeStringArray(pb *proto3.ListValue) ([]string, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("STRING")
|
||||
}
|
||||
a := make([]string, len(pb.Values))
|
||||
st := stringType()
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, st, &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "STRING", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeNullInt64Array decodes proto3.ListValue pb into a NullInt64 slice.
|
||||
func decodeNullInt64Array(pb *proto3.ListValue) ([]NullInt64, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("INT64")
|
||||
}
|
||||
@@ -767,8 +895,22 @@ func decodeIntArray(pb *proto3.ListValue) ([]NullInt64, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeBoolArray decodes proto3.ListValue pb into a NullBool slice.
|
||||
func decodeBoolArray(pb *proto3.ListValue) ([]NullBool, error) {
|
||||
// decodeInt64Array decodes proto3.ListValue pb into a int64 slice.
|
||||
func decodeInt64Array(pb *proto3.ListValue) ([]int64, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("INT64")
|
||||
}
|
||||
a := make([]int64, len(pb.Values))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, intType(), &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "INT64", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeNullBoolArray decodes proto3.ListValue pb into a NullBool slice.
|
||||
func decodeNullBoolArray(pb *proto3.ListValue) ([]NullBool, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("BOOL")
|
||||
}
|
||||
@@ -781,8 +923,22 @@ func decodeBoolArray(pb *proto3.ListValue) ([]NullBool, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeFloat64Array decodes proto3.ListValue pb into a NullFloat64 slice.
|
||||
func decodeFloat64Array(pb *proto3.ListValue) ([]NullFloat64, error) {
|
||||
// decodeBoolArray decodes proto3.ListValue pb into a bool slice.
|
||||
func decodeBoolArray(pb *proto3.ListValue) ([]bool, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("BOOL")
|
||||
}
|
||||
a := make([]bool, len(pb.Values))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, boolType(), &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "BOOL", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeNullFloat64Array decodes proto3.ListValue pb into a NullFloat64 slice.
|
||||
func decodeNullFloat64Array(pb *proto3.ListValue) ([]NullFloat64, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("FLOAT64")
|
||||
}
|
||||
@@ -795,6 +951,20 @@ func decodeFloat64Array(pb *proto3.ListValue) ([]NullFloat64, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeFloat64Array decodes proto3.ListValue pb into a float64 slice.
|
||||
func decodeFloat64Array(pb *proto3.ListValue) ([]float64, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("FLOAT64")
|
||||
}
|
||||
a := make([]float64, len(pb.Values))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, floatType(), &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "FLOAT64", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeByteArray decodes proto3.ListValue pb into a slice of byte slice.
|
||||
func decodeByteArray(pb *proto3.ListValue) ([][]byte, error) {
|
||||
if pb == nil {
|
||||
@@ -809,8 +979,8 @@ func decodeByteArray(pb *proto3.ListValue) ([][]byte, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeTimeArray decodes proto3.ListValue pb into a NullTime slice.
|
||||
func decodeTimeArray(pb *proto3.ListValue) ([]NullTime, error) {
|
||||
// decodeNullTimeArray decodes proto3.ListValue pb into a NullTime slice.
|
||||
func decodeNullTimeArray(pb *proto3.ListValue) ([]NullTime, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("TIMESTAMP")
|
||||
}
|
||||
@@ -823,8 +993,22 @@ func decodeTimeArray(pb *proto3.ListValue) ([]NullTime, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeDateArray decodes proto3.ListValue pb into a NullDate slice.
|
||||
func decodeDateArray(pb *proto3.ListValue) ([]NullDate, error) {
|
||||
// decodeTimeArray decodes proto3.ListValue pb into a time.Time slice.
|
||||
func decodeTimeArray(pb *proto3.ListValue) ([]time.Time, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("TIMESTAMP")
|
||||
}
|
||||
a := make([]time.Time, len(pb.Values))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, timeType(), &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "TIMESTAMP", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeNullDateArray decodes proto3.ListValue pb into a NullDate slice.
|
||||
func decodeNullDateArray(pb *proto3.ListValue) ([]NullDate, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("DATE")
|
||||
}
|
||||
@@ -837,6 +1021,20 @@ func decodeDateArray(pb *proto3.ListValue) ([]NullDate, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeDateArray decodes proto3.ListValue pb into a civil.Date slice.
|
||||
func decodeDateArray(pb *proto3.ListValue) ([]civil.Date, error) {
|
||||
if pb == nil {
|
||||
return nil, errNilListValue("DATE")
|
||||
}
|
||||
a := make([]civil.Date, len(pb.Values))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, dateType(), &a[i]); err != nil {
|
||||
return nil, errDecodeArrayElement(i, v, "DATE", err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func errNotStructElement(i int, v *proto3.Value) error {
|
||||
return errDecodeArrayElement(i, v, "STRUCT",
|
||||
spannerErrorf(codes.FailedPrecondition, "%v(type: %T) doesn't encode Cloud Spanner STRUCT", v, v))
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build go1.7
|
||||
|
||||
package spanner
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/civil"
|
||||
proto3 "github.com/golang/protobuf/ptypes/struct"
|
||||
sppb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
)
|
||||
|
||||
func BenchmarkEncodeIntArray(b *testing.B) {
|
||||
for _, s := range []struct {
|
||||
name string
|
||||
f func(a []int) (*proto3.Value, *sppb.Type, error)
|
||||
}{
|
||||
{"Orig", encodeIntArrayOrig},
|
||||
{"Func", encodeIntArrayFunc},
|
||||
{"Reflect", encodeIntArrayReflect},
|
||||
} {
|
||||
b.Run(s.name, func(b *testing.B) {
|
||||
for _, size := range []int{1, 10, 100, 1000} {
|
||||
a := make([]int, size)
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
s.f(a)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIntArrayOrig(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
vs := make([]*proto3.Value, len(a))
|
||||
var err error
|
||||
for i := range a {
|
||||
vs[i], _, err = encodeValue(a[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return listProto(vs...), listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeIntArrayFunc(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
v, err := encodeArray(len(a), func(i int) interface{} { return a[i] })
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return v, listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeIntArrayReflect(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
v, err := encodeArrayReflect(a)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return v, listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeArrayReflect(a interface{}) (*proto3.Value, error) {
|
||||
va := reflect.ValueOf(a)
|
||||
len := va.Len()
|
||||
vs := make([]*proto3.Value, len)
|
||||
var err error
|
||||
for i := 0; i < len; i++ {
|
||||
vs[i], _, err = encodeValue(va.Index(i).Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return listProto(vs...), nil
|
||||
}
|
||||
|
||||
func BenchmarkDecodeGeneric(b *testing.B) {
|
||||
v := stringProto("test")
|
||||
t := stringType()
|
||||
var g GenericColumnValue
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
decodeValue(v, t, &g)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeArray(b *testing.B) {
|
||||
for _, size := range []int{1, 10, 100, 1000} {
|
||||
vals := make([]*proto3.Value, size)
|
||||
for i := 0; i < size; i++ {
|
||||
vals[i] = dateProto(d1)
|
||||
}
|
||||
lv := &proto3.ListValue{Values: vals}
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
for _, s := range []struct {
|
||||
name string
|
||||
decode func(*proto3.ListValue)
|
||||
}{
|
||||
{"DateDirect", decodeArray_Date_direct},
|
||||
{"DateFunc", decodeArray_Date_func},
|
||||
{"DateReflect", decodeArray_Date_reflect},
|
||||
{"StringDecodeStringArray", decodeStringArrayWrap},
|
||||
{"StringDirect", decodeArray_String_direct},
|
||||
{"StringFunc", decodeArray_String_func},
|
||||
{"StringReflect", decodeArray_String_reflect},
|
||||
} {
|
||||
b.Run(s.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
s.decode(lv)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_Date_direct(pb *proto3.ListValue) {
|
||||
a := make([]civil.Date, len(pb.Values))
|
||||
t := dateType()
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, t, &a[i]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_Date_func(pb *proto3.ListValue) {
|
||||
a := make([]civil.Date, len(pb.Values))
|
||||
if err := decodeArray_func(pb, "DATE", dateType(), func(i int) interface{} { return &a[i] }); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_Date_reflect(pb *proto3.ListValue) {
|
||||
var a []civil.Date
|
||||
if err := decodeArray_reflect(pb, "DATE", dateType(), &a); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeStringArrayWrap(pb *proto3.ListValue) {
|
||||
if _, err := decodeStringArray(pb); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_String_direct(pb *proto3.ListValue) {
|
||||
a := make([]string, len(pb.Values))
|
||||
t := stringType()
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, t, &a[i]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_String_func(pb *proto3.ListValue) {
|
||||
|
||||
a := make([]string, len(pb.Values))
|
||||
if err := decodeArray_func(pb, "STRING", stringType(), func(i int) interface{} { return &a[i] }); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_String_reflect(pb *proto3.ListValue) {
|
||||
var a []string
|
||||
if err := decodeArray_reflect(pb, "STRING", stringType(), &a); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray_func(pb *proto3.ListValue, name string, typ *sppb.Type, elptr func(int) interface{}) error {
|
||||
if pb == nil {
|
||||
return errNilListValue(name)
|
||||
}
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, typ, elptr(i)); err != nil {
|
||||
return errDecodeArrayElement(i, v, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeArray_reflect(pb *proto3.ListValue, name string, typ *sppb.Type, aptr interface{}) error {
|
||||
if pb == nil {
|
||||
return errNilListValue(name)
|
||||
}
|
||||
av := reflect.ValueOf(aptr).Elem()
|
||||
av.Set(reflect.MakeSlice(av.Type(), len(pb.Values), len(pb.Values)))
|
||||
for i, v := range pb.Values {
|
||||
if err := decodeValue(v, typ, av.Index(i).Addr().Interface()); err != nil {
|
||||
av.Set(reflect.Zero(av.Type())) // reset slice to nil
|
||||
return errDecodeArrayElement(i, v, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+29
-126
@@ -23,7 +23,6 @@ import (
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/civil"
|
||||
"github.com/golang/protobuf/proto"
|
||||
proto3 "github.com/golang/protobuf/ptypes/struct"
|
||||
sppb "google.golang.org/genproto/googleapis/spanner/v1"
|
||||
)
|
||||
@@ -160,7 +159,7 @@ func TestDecodeValue(t *testing.T) {
|
||||
{nullProto(), stringType(), "abc", true},
|
||||
{stringProto("abc"), stringType(), NullString{"abc", true}, false},
|
||||
{nullProto(), stringType(), NullString{}, false},
|
||||
// STRING ARRAY
|
||||
// STRING ARRAY with []NullString
|
||||
{
|
||||
listProto(stringProto("abc"), nullProto(), stringProto("bcd")),
|
||||
listType(stringType()),
|
||||
@@ -168,6 +167,13 @@ func TestDecodeValue(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
{nullProto(), listType(stringType()), []NullString(nil), false},
|
||||
// STRING ARRAY with []string
|
||||
{
|
||||
listProto(stringProto("abc"), stringProto("bcd")),
|
||||
listType(stringType()),
|
||||
[]string{"abc", "bcd"},
|
||||
false,
|
||||
},
|
||||
// BYTES
|
||||
{bytesProto([]byte("ab")), bytesType(), []byte("ab"), false},
|
||||
{nullProto(), bytesType(), []byte(nil), false},
|
||||
@@ -179,23 +185,27 @@ func TestDecodeValue(t *testing.T) {
|
||||
{nullProto(), intType(), int64(0), true},
|
||||
{intProto(15), intType(), NullInt64{15, true}, false},
|
||||
{nullProto(), intType(), NullInt64{}, false},
|
||||
// INT64 ARRAY
|
||||
// INT64 ARRAY with []NullInt64
|
||||
{listProto(intProto(91), nullProto(), intProto(87)), listType(intType()), []NullInt64{{91, true}, {}, {87, true}}, false},
|
||||
{nullProto(), listType(intType()), []NullInt64(nil), false},
|
||||
// INT64 ARRAY with []int64
|
||||
{listProto(intProto(91), intProto(87)), listType(intType()), []int64{91, 87}, false},
|
||||
// BOOL
|
||||
{boolProto(true), boolType(), true, false},
|
||||
{nullProto(), boolType(), true, true},
|
||||
{boolProto(true), boolType(), NullBool{true, true}, false},
|
||||
{nullProto(), boolType(), NullBool{}, false},
|
||||
// BOOL ARRAY
|
||||
// BOOL ARRAY with []NullBool
|
||||
{listProto(boolProto(true), boolProto(false), nullProto()), listType(boolType()), []NullBool{{true, true}, {false, true}, {}}, false},
|
||||
{nullProto(), listType(boolType()), []NullBool(nil), false},
|
||||
// BOOL ARRAY with []bool
|
||||
{listProto(boolProto(true), boolProto(false)), listType(boolType()), []bool{true, false}, false},
|
||||
// FLOAT64
|
||||
{floatProto(3.14), floatType(), 3.14, false},
|
||||
{nullProto(), floatType(), 0.00, true},
|
||||
{floatProto(3.14), floatType(), NullFloat64{3.14, true}, false},
|
||||
{nullProto(), floatType(), NullFloat64{}, false},
|
||||
// FLOAT64 ARRAY
|
||||
// FLOAT64 ARRAY with []NullFloat64
|
||||
{
|
||||
listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), nullProto(), floatProto(3.1)),
|
||||
listType(floatType()),
|
||||
@@ -203,20 +213,31 @@ func TestDecodeValue(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
{nullProto(), listType(floatType()), []NullFloat64(nil), false},
|
||||
// FLOAT64 ARRAY with []float64
|
||||
{
|
||||
listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), floatProto(3.1)),
|
||||
listType(floatType()),
|
||||
[]float64{math.Inf(1), math.Inf(-1), 3.1},
|
||||
false,
|
||||
},
|
||||
// TIMESTAMP
|
||||
{timeProto(t1), timeType(), t1, false},
|
||||
{timeProto(t1), timeType(), NullTime{t1, true}, false},
|
||||
{nullProto(), timeType(), NullTime{}, false},
|
||||
// TIMESTAMP ARRAY
|
||||
// TIMESTAMP ARRAY with []NullTime
|
||||
{listProto(timeProto(t1), timeProto(t2), timeProto(t3), nullProto()), listType(timeType()), []NullTime{{t1, true}, {t2, true}, {t3, true}, {}}, false},
|
||||
{nullProto(), listType(timeType()), []NullTime(nil), false},
|
||||
// TIMESTAMP ARRAY with []time.Time
|
||||
{listProto(timeProto(t1), timeProto(t2), timeProto(t3)), listType(timeType()), []time.Time{t1, t2, t3}, false},
|
||||
// DATE
|
||||
{dateProto(d1), dateType(), d1, false},
|
||||
{dateProto(d1), dateType(), NullDate{d1, true}, false},
|
||||
{nullProto(), dateType(), NullDate{}, false},
|
||||
// DATE ARRAY
|
||||
// DATE ARRAY with []NullDate
|
||||
{listProto(dateProto(d1), dateProto(d2), nullProto()), listType(dateType()), []NullDate{{d1, true}, {d2, true}, {}}, false},
|
||||
{nullProto(), listType(dateType()), []NullDate(nil), false},
|
||||
// DATE ARRAY with []civil.Date
|
||||
{listProto(dateProto(d1), dateProto(d2)), listType(dateType()), []civil.Date{d1, d2}, false},
|
||||
// STRUCT ARRAY
|
||||
// STRUCT schema is equal to the following Go struct:
|
||||
// type s struct {
|
||||
@@ -475,13 +496,8 @@ func TestGenericColumnValue(t *testing.T) {
|
||||
{GenericColumnValue{listType(intType()), listProto(intProto(91), nullProto(), intProto(87))}, []NullInt64{{91, true}, {}, {87, true}}, false},
|
||||
{GenericColumnValue{intType(), intProto(42)}, GenericColumnValue{intType(), intProto(42)}, false}, // trippy! :-)
|
||||
} {
|
||||
// We take a copy and mutate because we're paranoid about immutability.
|
||||
inCopy := GenericColumnValue{
|
||||
Type: proto.Clone(test.in.Type).(*sppb.Type),
|
||||
Value: proto.Clone(test.in.Value).(*proto3.Value),
|
||||
}
|
||||
gotp := reflect.New(reflect.TypeOf(test.want))
|
||||
if err := inCopy.Decode(gotp.Interface()); err != nil {
|
||||
if err := test.in.Decode(gotp.Interface()); err != nil {
|
||||
if !test.fail {
|
||||
t.Errorf("cannot decode %v to %v: %v", test.in, test.want, err)
|
||||
}
|
||||
@@ -490,13 +506,6 @@ func TestGenericColumnValue(t *testing.T) {
|
||||
if test.fail {
|
||||
t.Errorf("decoding %v to %v succeeds unexpectedly", test.in, test.want)
|
||||
}
|
||||
// mutations to inCopy should be invisible to gotp.
|
||||
inCopy.Type.Code = sppb.TypeCode_TIMESTAMP
|
||||
inCopy.Value.Kind = &proto3.Value_NumberValue{NumberValue: 999}
|
||||
got := reflect.Indirect(gotp).Interface()
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("unexpected decode result - got %v, want %v", got, test.want)
|
||||
}
|
||||
|
||||
// Test we can go backwards as well.
|
||||
v, err := newGenericColumnValue(test.want)
|
||||
@@ -507,111 +516,5 @@ func TestGenericColumnValue(t *testing.T) {
|
||||
if !reflect.DeepEqual(*v, test.in) {
|
||||
t.Errorf("unexpected encode result - got %v, want %v", v, test.in)
|
||||
}
|
||||
// If want is a GenericColumnValue, mutate its underlying value to validate
|
||||
// we have taken a deep copy.
|
||||
if gcv, ok := test.want.(GenericColumnValue); ok {
|
||||
gcv.Type.Code = sppb.TypeCode_TIMESTAMP
|
||||
gcv.Value.Kind = &proto3.Value_NumberValue{NumberValue: 999}
|
||||
if !reflect.DeepEqual(*v, test.in) {
|
||||
t.Errorf("expected deep copy - got %v, want %v", v, test.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runBench(b *testing.B, size int, f func(a []int) (*proto3.Value, *sppb.Type, error)) {
|
||||
a := make([]int, size)
|
||||
for i := 0; i < b.N; i++ {
|
||||
f(a)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayOrig1(b *testing.B) {
|
||||
runBench(b, 1, encodeIntArrayOrig)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayOrig10(b *testing.B) {
|
||||
runBench(b, 10, encodeIntArrayOrig)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayOrig100(b *testing.B) {
|
||||
runBench(b, 100, encodeIntArrayOrig)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayOrig1000(b *testing.B) {
|
||||
runBench(b, 1000, encodeIntArrayOrig)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayFunc1(b *testing.B) {
|
||||
runBench(b, 1, encodeIntArrayFunc)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayFunc10(b *testing.B) {
|
||||
runBench(b, 10, encodeIntArrayFunc)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayFunc100(b *testing.B) {
|
||||
runBench(b, 100, encodeIntArrayFunc)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayFunc1000(b *testing.B) {
|
||||
runBench(b, 1000, encodeIntArrayFunc)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayReflect1(b *testing.B) {
|
||||
runBench(b, 1, encodeIntArrayReflect)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayReflect10(b *testing.B) {
|
||||
runBench(b, 10, encodeIntArrayReflect)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayReflect100(b *testing.B) {
|
||||
runBench(b, 100, encodeIntArrayReflect)
|
||||
}
|
||||
|
||||
func BenchmarkEncodeIntArrayReflect1000(b *testing.B) {
|
||||
runBench(b, 1000, encodeIntArrayReflect)
|
||||
}
|
||||
|
||||
func encodeIntArrayOrig(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
vs := make([]*proto3.Value, len(a))
|
||||
var err error
|
||||
for i := range a {
|
||||
vs[i], _, err = encodeValue(a[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return listProto(vs...), listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeIntArrayFunc(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
v, err := encodeArray(len(a), func(i int) interface{} { return a[i] })
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return v, listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeIntArrayReflect(a []int) (*proto3.Value, *sppb.Type, error) {
|
||||
v, err := encodeArrayReflect(a)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return v, listType(intType()), nil
|
||||
}
|
||||
|
||||
func encodeArrayReflect(a interface{}) (*proto3.Value, error) {
|
||||
va := reflect.ValueOf(a)
|
||||
len := va.Len()
|
||||
vs := make([]*proto3.Value, len)
|
||||
var err error
|
||||
for i := 0; i < len; i++ {
|
||||
vs[i], _, err = encodeValue(va.Index(i).Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return listProto(vs...), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user