From e864652b33a85fcc240b2690220b97ca31ef85f1 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Mon, 25 Apr 2022 16:07:46 -0400 Subject: [PATCH] feat(sessions): Add list benchmarks The list benchmark tests uses pre-created database dumps to run its benchmarks. This benchmark will be used to guide optimizations around listing of sessions. Fixes https://hashicorp.atlassian.net/browse/ICU-4042 --- Makefile | 13 ++ go.mod | 6 +- .../sessions/session_list_benchmarks_test.go | 188 ++++++++++++++++++ testing/dbtest/template.go | 40 +++- 4 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 internal/servers/controller/handlers/sessions/session_list_benchmarks_test.go diff --git a/Makefile b/Makefile index 0296e5f6f..23fcff1e4 100644 --- a/Makefile +++ b/Makefile @@ -239,6 +239,19 @@ test-api: .PHONY: test-all test-all: test-sdk test-api test +BENCH_TIME?=1s +BENCH_COUNT?=1s +.PHONY: benchmark +benchmark: + @go test \ + -timeout 60m \ + ./internal/servers/controller/handlers/sessions/ \ + -v \ + -bench '^BenchmarkSessionList$$' \ + -benchtime $(BENCH_TIME) \ + -count $(BENCH_COUNT) \ + -run '^$$' + .PHONY: install-go install-go: ./ci/goinstall.sh diff --git a/go.mod b/go.mod index f84c2a77a..498c9104a 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,10 @@ require ( require github.com/hashicorp/go-dbw v0.0.0-20211215222256-2ff0d37184ff // this is a branch and should be updated before merging -require github.com/prometheus/client_golang v1.12.1 +require ( + github.com/prometheus/client_golang v1.12.1 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c +) require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect @@ -177,7 +180,6 @@ require ( golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/internal/servers/controller/handlers/sessions/session_list_benchmarks_test.go b/internal/servers/controller/handlers/sessions/session_list_benchmarks_test.go new file mode 100644 index 000000000..f46c1b231 --- /dev/null +++ b/internal/servers/controller/handlers/sessions/session_list_benchmarks_test.go @@ -0,0 +1,188 @@ +package sessions_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/controller/auth" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/sessions" + "github.com/hashicorp/boundary/internal/session" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/testing/dbtest" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +type template struct { + name string + sessions int + connsPerSession int + users int +} + +func BenchmarkSessionList(b *testing.B) { + // See the explanation in testing/dbtest/session_list_benchmarks_dump_generation_test.go for + // an overview of the assumptions made when creating these template scenarios. + for _, template := range []template{ + { + name: dbtest.Boundary1000Sessions10ConnsPerSession10Template, + sessions: 1000, + connsPerSession: 10, + users: 10, + }, + { + name: dbtest.Boundary1000Sessions10ConnsPerSession25Template, + sessions: 1000, + connsPerSession: 10, + users: 25, + }, + { + name: dbtest.Boundary1000Sessions10ConnsPerSession50Template, + sessions: 1000, + connsPerSession: 10, + users: 50, + }, + { + name: dbtest.Boundary1000Sessions10ConnsPerSession75Template, + sessions: 1000, + connsPerSession: 10, + users: 75, + }, + { + name: dbtest.Boundary1000Sessions10ConnsPerSession100Template, + sessions: 1000, + connsPerSession: 10, + users: 100, + }, + { + name: dbtest.Boundary1000Sessions10ConnsPerSession500Template, + sessions: 1000, + connsPerSession: 10, + users: 500, + }, + } { + b.Run(fmt.Sprintf("%d_sessions_%d_conns_per_session_%d_users", template.sessions, template.connsPerSession, template.users), func(b *testing.B) { + ctx := context.Background() + conn, _ := db.TestSetup(b, "postgres", db.WithTemplate(template.name)) + rw := db.New(conn) + kmsRepo, err := kms.NewRepository(rw, rw) + require.NoError(b, err) + kmsThing, err := kms.NewKms(kmsRepo) + require.NoError(b, err) + wrap, err := dbtest.GetBoundaryBenchmarksRootKeyWrapper(ctx) + require.NoError(b, err) + err = kmsThing.AddExternalWrappers(ctx, kms.WithRootWrapper(wrap)) + require.NoError(b, err) + + iamRepo, err := iam.NewRepository(rw, rw, kmsThing) + require.NoError(b, err) + + sessRepo, err := session.NewRepository(rw, rw, kmsThing) + require.NoError(b, err) + + authTokenRepo, err := authtoken.NewRepository(rw, rw, kmsThing) + require.NoError(b, err) + + pwRepo, err := password.NewRepository(rw, rw, kmsThing) + require.NoError(b, err) + + serversRepo, err := servers.NewRepository(rw, rw, kmsThing) + require.NoError(b, err) + + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + sessRepoFn := func() (*session.Repository, error) { + return sessRepo, nil + } + authTokenRepoFn := func() (*authtoken.Repository, error) { + return authTokenRepo, nil + } + serversRepoFn := func() (*servers.Repository, error) { + return serversRepo, nil + } + + s, err := sessions.NewService(sessRepoFn, iamRepoFn) + require.NoError(b, err) + + var users []*userWithToken + rows, err := rw.Query(ctx, "select public_id from iam_user where name like 'user%'", nil) + require.NoError(b, err) + var userId string + for rows.Next() { + err = rows.Scan(&userId) + require.NoError(b, err) + require.NotEmpty(b, userId) + u, accountIds, err := iamRepo.LookupUser(ctx, userId) + require.NoError(b, err) + require.NotEmpty(b, accountIds) + account, err := pwRepo.LookupAccount(ctx, accountIds[0]) + require.NoError(b, err) + require.NotNil(b, account) + acct, err := pwRepo.Authenticate(ctx, u.ScopeId, account.AuthMethodId, u.Name, dbtest.BoundaryBenchmarksUserPassword) + require.NoError(b, err) + require.NotNil(b, acct) + tok, err := authTokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) + require.NoError(b, err) + require.NotEmpty(b, tok) + tokString, err := authtoken.EncryptToken(ctx, kmsThing, u.ScopeId, tok.PublicId, tok.Token) + require.NoError(b, err) + req := &pbs.ListSessionsRequest{ + ScopeId: scope.Global.String(), + Recursive: true, + Filter: fmt.Sprintf(`"/item/user_id"==%q`, u.PublicId), + } + ctx := auth.NewVerifierContext( + ctx, + iamRepoFn, + authTokenRepoFn, + serversRepoFn, + kmsThing, + &authpb.RequestInfo{ + PublicId: tok.PublicId, + EncryptedToken: tokString, + TokenFormat: uint32(auth.AuthTokenTypeBearer), + }) + users = append(users, &userWithToken{ + User: u, + req: req, + ctx: ctx, + }) + } + require.NoError(b, rows.Close()) + require.NoError(b, rows.Err()) + require.Len(b, users, template.users) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + eg := &errgroup.Group{} + for i := range users { + user := users[i] + eg.Go(func() error { + _, err := s.ListSessions(user.ctx, user.req) + if err != nil { + return fmt.Errorf("list failed: %s", err.Error()) + } + return nil + }) + } + require.NoError(b, eg.Wait()) + } + }) + } +} + +type userWithToken struct { + *iam.User + req *pbs.ListSessionsRequest + ctx context.Context +} diff --git a/testing/dbtest/template.go b/testing/dbtest/template.go index 5a3a9892e..7178b2d1d 100644 --- a/testing/dbtest/template.go +++ b/testing/dbtest/template.go @@ -1,6 +1,8 @@ package dbtest import ( + "context" + "encoding/base64" "fmt" "math/rand" "os" @@ -9,6 +11,8 @@ import ( // postgres dialect "github.com/hashicorp/boundary/internal/db/common" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/aead" _ "github.com/jackc/pgx/v4/stdlib" ) @@ -31,10 +35,20 @@ const ( Postgres = "postgres" ) +// BoundaryBenchmarksUserPassword is the password used for all users +// created by the boundary benchmarks dump generator. +const BoundaryBenchmarksUserPassword = "testpassword" + // Template Database Names const ( - BoundaryTemplate = "boundary_template" - Template1 = "template1" + BoundaryTemplate = "boundary_template" + Template1 = "template1" + Boundary1000Sessions10ConnsPerSession10Template = "boundary_session_1000_10_10_template" + Boundary1000Sessions10ConnsPerSession25Template = "boundary_session_1000_10_25_template" + Boundary1000Sessions10ConnsPerSession50Template = "boundary_session_1000_10_50_template" + Boundary1000Sessions10ConnsPerSession75Template = "boundary_session_1000_10_75_template" + Boundary1000Sessions10ConnsPerSession100Template = "boundary_session_1000_10_100_template" + Boundary1000Sessions10ConnsPerSession500Template = "boundary_session_1000_10_500_template" ) var supportedDialects = map[string]struct{}{ @@ -44,6 +58,12 @@ var supportedDialects = map[string]struct{}{ var supportedTemplates = map[string]struct{}{ BoundaryTemplate: {}, Template1: {}, + Boundary1000Sessions10ConnsPerSession10Template: {}, + Boundary1000Sessions10ConnsPerSession25Template: {}, + Boundary1000Sessions10ConnsPerSession50Template: {}, + Boundary1000Sessions10ConnsPerSession75Template: {}, + Boundary1000Sessions10ConnsPerSession100Template: {}, + Boundary1000Sessions10ConnsPerSession500Template: {}, } const letterBytes = "abcdefghijklmnopqrstuvwxyz" @@ -53,6 +73,22 @@ const ( letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits ) +const boundaryBenchmarksRootKey = "rootkey1rootkey1rootkey1rootkey1" // 32 bytes for AES-256 + +// GetBoundaryBenchmarksRootKeyWrapper returns the root key wrapper used for the benchmark dumps. +func GetBoundaryBenchmarksRootKeyWrapper(ctx context.Context) (wrapping.Wrapper, error) { + wrap := aead.NewWrapper() + _, err := wrap.SetConfig(ctx, wrapping.WithKeyId(base64.StdEncoding.EncodeToString([]byte(boundaryBenchmarksRootKey)))) + if err != nil { + return nil, err + } + err = wrap.SetAesGcmKeyBytes([]byte(boundaryBenchmarksRootKey)) + if err != nil { + return nil, err + } + return wrap, nil +} + func randStr(n int) string { b := make([]byte, n) // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!