blob: e11888dbede1d089bfde9233f432043fa1f06051 [file] [log] [blame]
// Copyright 2020 Google LLC
//
// 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
//
// https://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 database
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"unicode"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
longrunningpb "google.golang.org/genproto/googleapis/longrunning"
databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var retryer = gax.OnCodes(
[]codes.Code{codes.DeadlineExceeded, codes.Unavailable},
gax.Backoff{Initial: time.Millisecond, Max: time.Millisecond, Multiplier: 1.0},
)
// CreateDatabaseWithRetry creates a new database and retries the call if the
// backend returns a retryable error. The actual CreateDatabase RPC is only
// retried if the initial call did not reach the server. In other cases, the
// client will query the backend for the long-running operation that was
// created by the initial RPC and return that operation.
func (c *DatabaseAdminClient) CreateDatabaseWithRetry(ctx context.Context, req *databasepb.CreateDatabaseRequest, opts ...gax.CallOption) (*CreateDatabaseOperation, error) {
for {
db, createErr := c.CreateDatabase(ctx, req, opts...)
if createErr == nil {
return db, nil
}
// Failed, check whether we should retry.
delay, shouldRetry := retryer.Retry(createErr)
if !shouldRetry {
return nil, createErr
}
if err := gax.Sleep(ctx, delay); err != nil {
return nil, err
}
// Extract the name of the database.
dbName := extractDBName(req.CreateStatement)
// Query the backend for any corresponding long-running operation to
// determine whether we should retry the RPC or not.
iter := c.ListDatabaseOperations(ctx, &databasepb.ListDatabaseOperationsRequest{
Parent: req.Parent,
Filter: fmt.Sprintf("(metadata.@type:type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata) AND (name:%s/databases/%s/operations/)", req.Parent, dbName),
}, opts...)
var mostRecentOp *longrunningpb.Operation
for {
op, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
// A running operation is the most recent and should be returned.
if !op.Done {
return c.CreateDatabaseOperation(op.Name), nil
}
if op.GetError() == nil {
mostRecentOp = op
}
}
if mostRecentOp == nil {
continue
}
// Only finished operations found. Check whether the database exists.
_, getErr := c.GetDatabase(ctx, &databasepb.GetDatabaseRequest{
Name: fmt.Sprintf("%s/databases/%s", req.Parent, dbName),
})
if getErr == nil {
// Database found, return one of the long-running operations that
// has finished, which again should return the database.
return c.CreateDatabaseOperation(mostRecentOp.Name), nil
}
if status.Code(getErr) == codes.NotFound {
continue
}
// Error getting the database that was not NotFound.
return nil, getErr
}
}
var dbNameRegEx = regexp.MustCompile("\\s*CREATE\\s+DATABASE\\s+(.+)\\s*")
// extractDBName extracts the database name from a valid CREATE DATABASE <db>
// statement. We don't have to worry about invalid create statements, as those
// should already have been handled by the backend and should return a non-
// retryable error.
func extractDBName(createStatement string) string {
if dbNameRegEx.MatchString(createStatement) {
namePossiblyWithQuotes := strings.TrimRightFunc(dbNameRegEx.FindStringSubmatch(createStatement)[1], unicode.IsSpace)
if len(namePossiblyWithQuotes) > 0 && namePossiblyWithQuotes[0] == '`' {
if len(namePossiblyWithQuotes) > 5 && namePossiblyWithQuotes[1] == '`' && namePossiblyWithQuotes[2] == '`' {
return string(namePossiblyWithQuotes[3 : len(namePossiblyWithQuotes)-3])
}
return string(namePossiblyWithQuotes[1 : len(namePossiblyWithQuotes)-1])
}
return string(namePossiblyWithQuotes)
}
return ""
}