/* 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() }