| /* |
| 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 ( |
| "context" |
| "errors" |
| "sync" |
| "testing" |
| "time" |
| |
| "cloud.google.com/go/spanner/internal/testutil" |
| 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() |
| } |