open-nomad/helper/users/cache_test.go
Seth Hoenig af28ac1610
users: create cache for user lookups (#16100)
* users: create cache for user lookups

This PR introduces a global cache for OS user lookups. This should
relieve pressure on the OS domain/directory lookups, which would be
queried more now that Task API exists.

Hits are cached for 1 hour, and misses are cached for 1 minute. These
values are fairly arbitrary - we can tweak them if there is any reason to.

Closes #16010

* users: delete expired negative entry from cache
2023-02-09 08:37:50 -06:00

154 lines
3.1 KiB
Go

//go:build unix
package users
import (
"errors"
"os/user"
"testing"
"time"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
"oss.indeed.com/go/libtime/libtimetest"
)
func TestCache_real_hit(t *testing.T) {
ci.Parallel(t)
c := newCache()
// fresh lookup
u, err := c.GetUser("nobody")
must.NoError(t, err)
must.NotNil(t, u)
// hit again, cached value
u2, err2 := c.GetUser("nobody")
must.NoError(t, err2)
must.NotNil(t, u2)
must.True(t, u == u2) // compare pointers
}
func TestCache_real_miss(t *testing.T) {
ci.Parallel(t)
c := newCache()
// fresh lookup
u, err := c.GetUser("doesnotexist")
must.Error(t, err)
must.Nil(t, u)
// hit again, cached value
u2, err2 := c.GetUser("doesnotexist")
must.Error(t, err2)
must.Nil(t, u2)
must.True(t, err == err2) // compare pointers
}
func TestCache_mock_hit(t *testing.T) {
ci.Parallel(t)
c := newCache()
lookupCount := 0
// hijack the underlying lookup function with our own mock
c.lookupUser = func(username string) (*user.User, error) {
lookupCount++
return &user.User{Name: username}, nil
}
// hijack the clock with our own mock
t0 := time.Now()
clockCount := 0
c.clock = libtimetest.NewClockMock(t).NowMock.Set(func() time.Time {
clockCount++
switch clockCount {
case 1:
return t0
case 2:
return t0.Add(59 * time.Minute)
default:
return t0.Add(61 * time.Minute)
}
})
const username = "armon"
// initial lookup
u, err := c.GetUser(username)
must.NoError(t, err)
must.Eq(t, "armon", u.Name)
must.Eq(t, 1, lookupCount)
must.Eq(t, 1, clockCount)
// second lookup, 59 minutes after initil lookup
u2, err2 := c.GetUser(username)
must.NoError(t, err2)
must.Eq(t, "armon", u2.Name)
must.Eq(t, 1, lookupCount) // was in cache
must.Eq(t, 2, clockCount)
// third lookup, 61 minutes after initial lookup (expired)
u3, err3 := c.GetUser(username)
must.NoError(t, err3)
must.Eq(t, "armon", u3.Name)
must.Eq(t, 2, lookupCount)
must.Eq(t, 3, clockCount)
}
func TestCache_mock_miss(t *testing.T) {
ci.Parallel(t)
c := newCache()
lookupCount := 0
lookupErr := errors.New("lookup error")
// hijack the underlying lookup function with our own mock
c.lookupUser = func(username string) (*user.User, error) {
lookupCount++
return nil, lookupErr
}
// hijack the clock with our own mock
t0 := time.Now()
clockCount := 0
c.clock = libtimetest.NewClockMock(t).NowMock.Set(func() time.Time {
clockCount++
switch clockCount {
case 1:
return t0
case 2:
return t0.Add(59 * time.Second)
default:
return t0.Add(61 * time.Second)
}
})
const username = "armon"
// initial lookup
u, err := c.GetUser(username)
must.ErrorIs(t, err, lookupErr)
must.Nil(t, u)
must.Eq(t, 1, lookupCount)
must.Eq(t, 1, clockCount)
// second lookup, 59 seconds after initial (still in cache)
u2, err2 := c.GetUser(username)
must.ErrorIs(t, err2, lookupErr)
must.Nil(t, u2)
must.Eq(t, 1, lookupCount) // in cache
must.Eq(t, 2, clockCount)
// third lookup, 61 seconds after initial (expired)
u3, err3 := c.GetUser(username)
must.ErrorIs(t, err3, lookupErr)
must.Nil(t, u3)
must.Eq(t, 2, lookupCount)
must.Eq(t, 3, clockCount)
}