auth: Fix "race" - auth unit tests leaking goroutines

- We were leaking goroutines in auth-test
  - The go-routines were depending / modifying global test environment
variables (simpleTokenTTLDefault) leading to races

Removed the leaked go-routines, and expanded 'auth' package to
be covered we leaked go-routines detection.
This commit is contained in:
Piotr Tabor 2020-10-01 16:03:05 +02:00
parent 0e5d81704f
commit 528f5315d6
8 changed files with 50 additions and 39 deletions

15
auth/main_test.go Normal file
View File

@ -0,0 +1,15 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package auth
import (
"testing"
"go.etcd.io/etcd/v3/pkg/testutil"
)
func TestMain(m *testing.M) {
testutil.MustTestMainWithLeakDetection(m)
}

View File

@ -36,6 +36,7 @@ const (
) )
// var for testing purposes // var for testing purposes
// TODO: Remove this mutable global state - as it's race-prone.
var ( var (
simpleTokenTTLDefault = 300 * time.Second simpleTokenTTLDefault = 300 * time.Second
simpleTokenTTLResolution = 1 * time.Second simpleTokenTTLResolution = 1 * time.Second

View File

@ -50,6 +50,7 @@ func TestSimpleTokenDisabled(t *testing.T) {
func TestSimpleTokenAssign(t *testing.T) { func TestSimpleTokenAssign(t *testing.T) {
tp := newTokenProviderSimple(zap.NewExample(), dummyIndexWaiter, simpleTokenTTLDefault) tp := newTokenProviderSimple(zap.NewExample(), dummyIndexWaiter, simpleTokenTTLDefault)
tp.enable() tp.enable()
defer tp.disable()
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy") ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
token, err := tp.assign(ctx, "user1", 0) token, err := tp.assign(ctx, "user1", 0)
if err != nil { if err != nil {

View File

@ -64,10 +64,10 @@ func TestNewAuthStoreRevision(t *testing.T) {
// no changes to commit // no changes to commit
b2 := backend.NewDefaultBackend(tPath) b2 := backend.NewDefaultBackend(tPath)
defer b2.Close()
as = NewAuthStore(zap.NewExample(), b2, nil, tp, bcrypt.MinCost) as = NewAuthStore(zap.NewExample(), b2, nil, tp, bcrypt.MinCost)
defer as.Close()
new := as.Revision() new := as.Revision()
as.Close()
b2.Close()
if old != new { if old != new {
t.Fatalf("expected revision %d, got %d", old, new) t.Fatalf("expected revision %d, got %d", old, new)
@ -77,6 +77,7 @@ func TestNewAuthStoreRevision(t *testing.T) {
// TestNewAuthStoreBryptCost ensures that NewAuthStore uses default when given bcrypt-cost is invalid // TestNewAuthStoreBryptCost ensures that NewAuthStore uses default when given bcrypt-cost is invalid
func TestNewAuthStoreBcryptCost(t *testing.T) { func TestNewAuthStoreBcryptCost(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend() b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath) defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault) tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
@ -87,13 +88,11 @@ func TestNewAuthStoreBcryptCost(t *testing.T) {
invalidCosts := [2]int{bcrypt.MinCost - 1, bcrypt.MaxCost + 1} invalidCosts := [2]int{bcrypt.MinCost - 1, bcrypt.MaxCost + 1}
for _, invalidCost := range invalidCosts { for _, invalidCost := range invalidCosts {
as := NewAuthStore(zap.NewExample(), b, nil, tp, invalidCost) as := NewAuthStore(zap.NewExample(), b, nil, tp, invalidCost)
defer as.Close()
if as.BcryptCost() != bcrypt.DefaultCost { if as.BcryptCost() != bcrypt.DefaultCost {
t.Fatalf("expected DefaultCost when bcryptcost is invalid") t.Fatalf("expected DefaultCost when bcryptcost is invalid")
} }
as.Close()
} }
b.Close()
} }
func encodePassword(s string) string { func encodePassword(s string) string {
@ -175,6 +174,7 @@ func TestUserAdd(t *testing.T) {
func TestRecover(t *testing.T) { func TestRecover(t *testing.T) {
as, tearDown := setupAuthStore(t) as, tearDown := setupAuthStore(t)
defer as.Close()
defer tearDown(t) defer tearDown(t)
as.enabled = false as.enabled = false
@ -654,6 +654,7 @@ func TestIsAuthEnabled(t *testing.T) {
// TestAuthRevisionRace ensures that access to authStore.revision is thread-safe. // TestAuthRevisionRace ensures that access to authStore.revision is thread-safe.
func TestAuthInfoFromCtxRace(t *testing.T) { func TestAuthInfoFromCtxRace(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend() b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath) defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault) tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
@ -709,7 +710,8 @@ func TestIsAdminPermitted(t *testing.T) {
} }
func TestRecoverFromSnapshot(t *testing.T) { func TestRecoverFromSnapshot(t *testing.T) {
as, _ := setupAuthStore(t) as, teardown := setupAuthStore(t)
defer teardown(t)
ua := &pb.AuthUserAddRequest{Name: "foo", Options: &authpb.UserAddOptions{NoPassword: false}} ua := &pb.AuthUserAddRequest{Name: "foo", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err := as.UserAdd(ua) // add an existing user _, err := as.UserAdd(ua) // add an existing user
@ -733,9 +735,7 @@ func TestRecoverFromSnapshot(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
as2 := NewAuthStore(zap.NewExample(), as.be, nil, tp, bcrypt.MinCost) as2 := NewAuthStore(zap.NewExample(), as.be, nil, tp, bcrypt.MinCost)
defer func(a *authStore) { defer as2.Close()
a.Close()
}(as2)
if !as2.IsAuthEnabled() { if !as2.IsAuthEnabled() {
t.Fatal("recovering authStore from existing backend failed") t.Fatal("recovering authStore from existing backend failed")
@ -808,13 +808,16 @@ func TestHammerSimpleAuthenticate(t *testing.T) {
// TestRolesOrder tests authpb.User.Roles is sorted // TestRolesOrder tests authpb.User.Roles is sorted
func TestRolesOrder(t *testing.T) { func TestRolesOrder(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend() b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath) defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault) tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
defer tp.disable()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost) as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
defer as.Close()
err = enableAuthAndCreateRoot(as) err = enableAuthAndCreateRoot(as)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -863,6 +866,7 @@ func TestAuthInfoFromCtxWithRootJWT(t *testing.T) {
// testAuthInfoFromCtxWithRoot ensures "WithRoot" properly embeds token in the context. // testAuthInfoFromCtxWithRoot ensures "WithRoot" properly embeds token in the context.
func testAuthInfoFromCtxWithRoot(t *testing.T, opts string) { func testAuthInfoFromCtxWithRoot(t *testing.T, opts string) {
b, tPath := backend.NewDefaultTmpBackend() b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath) defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), opts, dummyIndexWaiter, simpleTokenTTLDefault) tp, err := NewTokenProvider(zap.NewExample(), opts, dummyIndexWaiter, simpleTokenTTLDefault)

View File

@ -5,16 +5,11 @@
package integration package integration
import ( import (
"os"
"testing" "testing"
"go.etcd.io/etcd/v3/pkg/testutil" "go.etcd.io/etcd/v3/pkg/testutil"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
v := m.Run() testutil.MustTestMainWithLeakDetection(m)
if v == 0 && testutil.CheckLeakedGoroutine() {
os.Exit(1)
}
os.Exit(v)
} }

View File

@ -5,16 +5,11 @@
package integration package integration
import ( import (
"os"
"testing" "testing"
"go.etcd.io/etcd/v3/pkg/testutil" "go.etcd.io/etcd/v3/pkg/testutil"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
v := m.Run() testutil.MustTestMainWithLeakDetection(m)
if v == 0 && testutil.CheckLeakedGoroutine() {
os.Exit(1)
}
os.Exit(v)
} }

View File

@ -5,16 +5,11 @@
package integration package integration
import ( import (
"os"
"testing" "testing"
"go.etcd.io/etcd/v3/pkg/testutil" "go.etcd.io/etcd/v3/pkg/testutil"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
v := m.Run() testutil.MustTestMainWithLeakDetection(m)
if v == 0 && testutil.CheckLeakedGoroutine() {
os.Exit(1)
}
os.Exit(v)
} }

View File

@ -24,11 +24,7 @@ running(leaking) after all tests.
import "go.etcd.io/etcd/v3/pkg/testutil" import "go.etcd.io/etcd/v3/pkg/testutil"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
v := m.Run() testutil.MustTestMainWithLeakDetection(m)
if v == 0 && testutil.CheckLeakedGoroutine() {
os.Exit(1)
}
os.Exit(v)
} }
func TestSample(t *testing.T) { func TestSample(t *testing.T) {
@ -38,10 +34,6 @@ running(leaking) after all tests.
*/ */
func CheckLeakedGoroutine() bool { func CheckLeakedGoroutine() bool {
if testing.Short() {
// not counting goroutines for leakage in -short mode
return false
}
gs := interestingGoroutines() gs := interestingGoroutines()
if len(gs) == 0 { if len(gs) == 0 {
return false return false
@ -66,9 +58,6 @@ func CheckLeakedGoroutine() bool {
// Waits for go-routines shutdown for 'd'. // Waits for go-routines shutdown for 'd'.
func CheckAfterTest(d time.Duration) error { func CheckAfterTest(d time.Duration) error {
http.DefaultTransport.(*http.Transport).CloseIdleConnections() http.DefaultTransport.(*http.Transport).CloseIdleConnections()
if testing.Short() {
return nil
}
var bad string var bad string
badSubstring := map[string]string{ badSubstring := map[string]string{
").writeLoop(": "a Transport", ").writeLoop(": "a Transport",
@ -140,3 +129,19 @@ func interestingGoroutines() (gs []string) {
sort.Strings(gs) sort.Strings(gs)
return gs return gs
} }
// MustTestMainWithLeakDetection expands standard m.Run with leaked
// goroutines detection.
func MustTestMainWithLeakDetection(m *testing.M) {
v := m.Run()
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
// Let the other goroutines finalize.
runtime.Gosched()
if v == 0 && CheckLeakedGoroutine() {
os.Exit(1)
}
os.Exit(v)
}