blob: a54a2e13c977d72a4fea4b7478d92e5fd132d8e9 [file] [log] [blame]
// Copyright 2016 The Upspin 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 usercache provides a KeyServer implementation that wraps
// another and caches Lookups.
// If a Lookup is made for the user that last Dialed the service,
// data from that user's config will be provided instead of making
// a request to the underlying server.
// The caching KeyServer will defer Dialing the underlying service
// until a Lookup or Put request needs to access that service.
package usercache // import "upspin.io/key/usercache"
import (
"sync"
"time"
"upspin.io/cache"
"upspin.io/errors"
"upspin.io/upspin"
)
type entry struct {
expires time.Time // when the information expires.
user *upspin.User
}
type userCacheServer struct {
cache *userCache
// The underlying key server.
base upspin.KeyServer
dd *deferredDial
}
// deferredDial is used to defer dialing the underlying
// service until a Lookup or Put call requires it.
// If config is non-nil, then the Dial method has been called.
// If dialed is non-nil, then the underlying service has been dialed.
type deferredDial struct {
mu sync.Mutex
config upspin.Config
endpoint upspin.Endpoint
dialed upspin.KeyServer
}
var _ upspin.KeyServer = (*userCacheServer)(nil)
type userCache struct {
entries *cache.LRU
duration time.Duration
}
const (
// defaultDuration is the default entry expiration time.
defaultDuration = 15 * time.Minute
// configUserDuration is the expiration time of the dialing user's
// pre-populated record. This is set to a decade to ensure that we
// always use the config's values, unless overridden by a Put.
configUserDuration = 3650 * 24 * time.Hour
)
var globalCache = userCache{entries: cache.NewLRU(256), duration: defaultDuration}
// Global returns the provided key server wrapped in a global user cache.
func Global(s upspin.KeyServer) upspin.KeyServer {
return &userCacheServer{
base: s,
cache: &globalCache,
}
}
// ResetGlobal resets the global cache.
func ResetGlobal() {
globalCache.entries = cache.NewLRU(256)
}
// Lookup implements upspin.KeyServer.
func (c *userCacheServer) Lookup(name upspin.UserName) (*upspin.User, error) {
const op errors.Op = "key/usercache.Lookup"
// If we have an unexpired cache entry, use it.
if v, ok := c.cache.entries.Get(name); ok {
if !time.Now().After(v.(*entry).expires) {
e := v.(*entry)
return e.user, nil
}
c.cache.entries.Remove(name)
}
// Not found, look it up.
if err := c.dial(); err != nil {
return nil, errors.E(op, err)
}
u, err := c.dd.dialed.Lookup(name)
if err != nil {
return nil, errors.E(op, err)
}
e := &entry{
expires: time.Now().Add(c.cache.duration),
user: u,
}
c.cache.entries.Add(name, e)
return u, nil
}
// Put implements upspin.KeyServer.
func (c *userCacheServer) Put(user *upspin.User) error {
const op errors.Op = "key/usercache.Put"
if err := c.dial(); err != nil {
return errors.E(op, err)
}
if err := c.dd.dialed.Put(user); err != nil {
return errors.E(op, err)
}
c.cache.entries.Remove(user.Name)
return nil
}
// Endpoint implements upspin.Service.
func (c *userCacheServer) Endpoint() upspin.Endpoint {
// We don't want Endpoint to trigger a Dial.
// Just return the Endpoint for either the dialed or base service.
c.dd.mu.Lock()
svc := c.dd.dialed
c.dd.mu.Unlock()
if svc != nil {
return svc.Endpoint()
}
return c.base.Endpoint()
}
// Authenticate implements upspin.Service.
func (c *userCacheServer) Authenticate(upspin.Config) error {
return errors.Str("key/usercache.Authenticate: not implemented")
}
// Close implements upspin.Service.
func (c *userCacheServer) Close() {
// If we're dialed, closed the dialed service.
c.dd.mu.Lock()
svc := c.dd.dialed
c.dd.mu.Unlock()
if svc != nil {
svc.Close()
return
}
// Otherwise, close the underlying service.
c.base.Close()
}
// Dial implements upspin.Dialer.
func (c *userCacheServer) Dial(cfg upspin.Config, e upspin.Endpoint) (upspin.Service, error) {
c.cacheConfigUser(cfg)
cc := *c
cc.dd = &deferredDial{
config: cfg,
endpoint: e,
}
return &cc, nil
}
// cacheConfigUser puts the dialed user in the cache with an extra-long expiry
// time, so that we don't hit the underlying cache for the current user and
// instead use the values from their config.
func (c *userCacheServer) cacheConfigUser(cfg upspin.Config) {
if cfg == nil {
return
}
f := cfg.Factotum()
if f == nil {
return
}
name := cfg.UserName()
c.cache.entries.Add(name, &entry{
expires: time.Now().Add(configUserDuration),
user: &upspin.User{
Name: name,
Dirs: []upspin.Endpoint{
cfg.DirEndpoint(),
},
Stores: []upspin.Endpoint{
cfg.StoreEndpoint(),
},
PublicKey: f.PublicKey(),
},
})
}
// dial dials the underlying key service using the arguments
// provided to the previous invocation of Dial.
// If Dial was not called, it returns an error.
// If there is already a dialed service, it does nothing.
func (c *userCacheServer) dial() error {
c.dd.mu.Lock()
defer c.dd.mu.Unlock()
if c.dd.dialed != nil {
return nil
}
if c.dd.config == nil {
return errors.Str("server not dialed")
}
svc, err := c.base.Dial(c.dd.config, c.dd.endpoint)
if err != nil {
return err
}
c.dd.dialed = svc.(upspin.KeyServer)
return nil
}