blob: a78049a622eef64e733d6f01ddcbb823ef8b8dc8 [file] [log] [blame]
/*
Copyright 2017 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
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"
"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()
mc.SetActions(
testutil.Action{"BeginTransaction", errUsr},
testutil.Action{"BeginTransaction", nil},
testutil.Action{"BeginTransaction", nil},
)
// 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(); !testEqual(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); !testEqual(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(); !testEqual(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 !testEqual(sh1.session, sh2.session) {
t.Errorf("Expect acquire to get same session handle, got %v and %v.", sh1, sh2)
}
if !testEqual(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
mc.SetActions(
testutil.Action{"Commit", errAbrt}, // abort on first commit
testutil.Action{"Commit", nil},
)
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
mc.SetActions(
testutil.Action{"BeginTransaction", nil}, // let takeWriteSession succeed and get a session handle
testutil.Action{"Commit", errAbrt}, // let first commit fail and retry will begin new transaction
testutil.Action{"BeginTransaction", errAbrt}, // this time we can fail the begin attempt
testutil.Action{"BeginTransaction", nil},
testutil.Action{"Commit", nil},
)
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
mc.SetActions(
testutil.Action{"BeginTransaction", wantErr},
testutil.Action{"BeginTransaction", wantErr},
testutil.Action{"BeginTransaction", wantErr},
)
txn := client.ReadOnlyTransaction()
defer txn.Close()
if _, _, got := txn.acquire(ctx); !testEqual(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")); !testEqual(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"}); !testEqual(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)}),
}
mc.SetActions(testutil.Action{"Commit", wantErr})
if _, got := client.Apply(context.Background(), ms, ApplyAtLeastOnce()); !testEqual(wantErr, got) {
t.Errorf("Expect applyAtLeastOnce to fail, got %v, want %v.", got, wantErr)
}
}
func TestFunctionErrorReturned(t *testing.T) {
t.Parallel()
_, mc, client := mockClient(t)
defer client.Close()
mc.SetActions(
testutil.Action{"BeginTransaction", nil},
testutil.Action{"Rollback", nil},
)
want := errors.New("an error")
_, got := client.ReadWriteTransaction(context.Background(),
func(context.Context, *ReadWriteTransaction) error { return want })
if got != want {
t.Errorf("got <%v>, want <%v>", got, want)
}
mc.CheckActionsConsumed()
}