af28ac1610
* 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
86 lines
1.8 KiB
Go
86 lines
1.8 KiB
Go
package users
|
|
|
|
import (
|
|
"os/user"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/lib/lang"
|
|
"oss.indeed.com/go/libtime"
|
|
)
|
|
|
|
const (
|
|
cacheTTL = 1 * time.Hour
|
|
failureTTL = 1 * time.Minute
|
|
)
|
|
|
|
type entry[T any] lang.Pair[T, time.Time]
|
|
|
|
func (e *entry[T]) expired(now time.Time, ttl time.Duration) bool {
|
|
return now.After(e.Second.Add(ttl))
|
|
}
|
|
|
|
type (
|
|
userCache map[string]*entry[*user.User]
|
|
userFailureCache map[string]*entry[error]
|
|
)
|
|
|
|
type lookupUserFunc func(string) (*user.User, error)
|
|
|
|
type cache struct {
|
|
clock libtime.Clock
|
|
lookupUser lookupUserFunc
|
|
|
|
lock sync.Mutex
|
|
users userCache
|
|
userFailures userFailureCache
|
|
}
|
|
|
|
func newCache() *cache {
|
|
return &cache{
|
|
clock: libtime.SystemClock(),
|
|
lookupUser: internalLookupUser,
|
|
users: make(userCache),
|
|
userFailures: make(userFailureCache),
|
|
}
|
|
}
|
|
|
|
func (c *cache) GetUser(username string) (*user.User, error) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// record this moment as "now" for further cache operations
|
|
now := c.clock.Now()
|
|
|
|
// first check if the user is in the cache and the entry we have
|
|
// is not yet expired
|
|
usr, exists := c.users[username]
|
|
if exists && !usr.expired(now, cacheTTL) {
|
|
return usr.First, nil
|
|
}
|
|
|
|
// next check if there was a recent failure already, so we
|
|
// avoid spamming the OS with dead user lookups
|
|
failure, exists2 := c.userFailures[username]
|
|
if exists2 {
|
|
if !failure.expired(now, failureTTL) {
|
|
return nil, failure.First
|
|
}
|
|
// may as well cleanup expired case
|
|
delete(c.userFailures, username)
|
|
}
|
|
|
|
// need to perform an OS lookup
|
|
u, err := c.lookupUser(username)
|
|
|
|
// lookup was a failure, populate the failure cache
|
|
if err != nil {
|
|
c.userFailures[username] = &entry[error]{err, now}
|
|
return nil, err
|
|
}
|
|
|
|
// lookup was a success, populate the user cache
|
|
c.users[username] = &entry[*user.User]{u, now}
|
|
return u, nil
|
|
}
|