Commit 08369cd4 authored by ale's avatar ale

Add a caching backend

It will cache User objects (not directly the resources yet), wrapping
another Backend object and invalidating the cache on every
state-changing API call.
parent 02df3660
package cachebackend
import (
"context"
"time"
"github.com/patrickmn/go-cache"
"golang.org/x/sync/singleflight"
as "git.autistici.org/ai3/accountserver"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
)
var (
defaultExpiration = 600 * time.Second
cleanupInterval = 180 * time.Second
)
// cacheBackend implements a simple in-memory cache of user objects
// (not resources yet), in order to reduce the database and processing
// load in presence of a heavily read-oriented workload. The cache is
// very simple, and any update to a user or its resources cause us to
// go back to the database backend.
//
// User objects are kept in memory for a short while and periodically
// cleaned up. Memory usage thus depends on the load and is difficult
// to estimate in advance.
//
type cacheBackend struct {
as.Backend
cache *cache.Cache
}
// Wrap a Backend with a cache.
func Wrap(b as.Backend) as.Backend {
c := cache.New(defaultExpiration, cleanupInterval)
return &cacheBackend{
Backend: b,
cache: c,
}
}
func (b *cacheBackend) NewTransaction() (as.TX, error) {
innerTX, err := b.Backend.NewTransaction()
if err != nil {
return nil, err
}
return &cacheTX{TX: innerTX, cache: b.cache}, nil
}
type cacheTX struct {
as.TX
cache *cache.Cache
}
func (c *cacheTX) invalidateUser(username string) {
c.cache.Delete(username)
}
var update singleflight.Group
func (c *cacheTX) GetUser(ctx context.Context, name string) (*as.RawUser, error) {
obj, ok := c.cache.Get(name)
if ok {
return obj.(*as.RawUser), nil
}
tmp, err, _ := update.Do(name, func() (interface{}, error) {
user, err := c.TX.GetUser(ctx, name)
if err != nil {
return nil, err
}
c.cache.Set(name, user, cache.DefaultExpiration)
return user, nil
})
if err != nil {
return nil, err
}
return tmp.(*as.RawUser), nil
}
func (c *cacheTX) UpdateUser(ctx context.Context, user *as.User) error {
err := c.TX.UpdateUser(ctx, user)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetUserPassword(ctx context.Context, user *as.User, pw string) error {
err := c.TX.SetUserPassword(ctx, user, pw)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetAccountRecoveryHint(ctx context.Context, user *as.User, rh, ra string) error {
err := c.TX.SetAccountRecoveryHint(ctx, user, rh, ra)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) DeleteAccountRecoveryHint(ctx context.Context, user *as.User) error {
err := c.TX.DeleteAccountRecoveryHint(ctx, user)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*ct.EncryptedKey) error {
err := c.TX.SetUserEncryptionKeys(ctx, user, keys)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.User, pubKey []byte) error {
err := c.TX.SetUserEncryptionPublicKey(ctx, user, pubKey)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *as.AppSpecificPasswordInfo, pw string) error {
err := c.TX.SetApplicationSpecificPassword(ctx, user, asp, pw)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) DeleteApplicationSpecificPassword(ctx context.Context, user *as.User, pw string) error {
err := c.TX.DeleteApplicationSpecificPassword(ctx, user, pw)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) SetUserTOTPSecret(ctx context.Context, user *as.User, secret string) error {
err := c.TX.SetUserTOTPSecret(ctx, user, secret)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) DeleteUserTOTPSecret(ctx context.Context, user *as.User) error {
err := c.TX.DeleteUserTOTPSecret(ctx, user)
if err == nil {
c.invalidateUser(user.Name)
}
return err
}
func (c *cacheTX) UpdateResource(ctx context.Context, r *as.Resource) error {
err := c.TX.UpdateResource(ctx, r)
// TODO: invalidate user, but we currently do not know who it is!
return err
}
func (c *cacheTX) CreateResources(ctx context.Context, user *as.User, resources []*as.Resource) ([]*as.Resource, error) {
result, err := c.TX.CreateResources(ctx, user, resources)
if err == nil && user != nil {
c.invalidateUser(user.Name)
}
return result, err
}
//func (c *cacheTX) CreateUser(_ context.Context, user *as.User) (*as.User, error) {}
//func (c *cacheTX) GetResource(_ context.Context, id as.ResourceID) (*as.RawResource, error) {}
//func (c *cacheTX) SetResourcePassword(_ context.Context, r *as.Resource, pw string) error {}
//func (c *cacheTX) HasAnyResource(_ context.Context, f []as.FindResourceRequest) (bool, error) {}
//func (c *cacheTX) SearchUser(_ context.Context, string) ([]string, error) {}
//func (c *cacheTX) SearchResource(_ context.Context, string) ([]*as.RawResource, error) {}
//func (c *cacheTX) CanAccessResource(_ context.Context, principal string, r *as.Resource) bool {}
//func (c *cacheTX) NextUID(_ context.Context) (int, error) {}
......@@ -13,6 +13,7 @@ import (
"git.autistici.org/ai3/go-common/serverutil"
"gopkg.in/yaml.v2"
cachebackend "git.autistici.org/ai3/accountserver/backend/cache"
ldapbackend "git.autistici.org/ai3/accountserver/backend/ldap"
"git.autistici.org/ai3/accountserver/server"
)
......@@ -30,6 +31,9 @@ type config struct {
BindPwFile string `yaml:"bind_pw_file"`
BaseDN string `yaml:"base_dn"`
} `yaml:"ldap"`
Cache struct {
Enabled bool `yaml:"enabled"`
} `yaml:"cache"`
AccountServerConfig accountserver.Config `yaml:",inline"`
ServerConfig *serverutil.ServerConfig `yaml:"http_server"`
PwHash struct {
......@@ -159,6 +163,10 @@ func main() {
log.Fatal(err)
}
if config.Cache.Enabled {
be = cachebackend.Wrap(be)
}
service, err := accountserver.NewAccountService(be, &config.AccountServerConfig)
if err != nil {
log.Fatal(err)
......
......@@ -131,17 +131,28 @@ func runChangeUserPasswordTest(t *testing.T, username string, cfg as.Config) *as
}
func TestIntegration_AccountRecovery(t *testing.T) {
runAccountRecoveryTest(t, "uno@investici.org")
runAccountRecoveryTest(t, "uno@investici.org", false)
}
func TestIntegration_AccountRecovery_WithEncryptionKeys(t *testing.T) {
user := runAccountRecoveryTest(t, "due@investici.org")
user := runAccountRecoveryTest(t, "due@investici.org", false)
if !user.HasEncryptionKeys {
t.Fatalf("encryption keys not enabled after account recovery")
}
}
func runAccountRecoveryTest(t *testing.T, username string) *as.RawUser {
func TestIntegration_AccountRecovery_WithCache(t *testing.T) {
runAccountRecoveryTest(t, "uno@investici.org", true)
}
func TestIntegration_AccountRecovery_WithEncryptionKeysAndCache(t *testing.T) {
user := runAccountRecoveryTest(t, "due@investici.org", true)
if !user.HasEncryptionKeys {
t.Fatalf("encryption keys not enabled after account recovery")
}
}
func runAccountRecoveryTest(t *testing.T, username string, enableCache bool) *as.RawUser {
stop, be, c := startService(t)
defer stop()
......
......@@ -15,7 +15,8 @@ import (
"time"
as "git.autistici.org/ai3/accountserver"
"git.autistici.org/ai3/accountserver/backend"
cachebackend "git.autistici.org/ai3/accountserver/backend/cache"
ldapbackend "git.autistici.org/ai3/accountserver/backend/ldap"
"git.autistici.org/ai3/accountserver/ldaptest"
"git.autistici.org/ai3/accountserver/server"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
......@@ -45,7 +46,7 @@ func withSSO(t testing.TB) (func(), sso.Signer, string) {
if err != nil {
t.Fatal(err)
}
tmpf.Write(pub)
tmpf.Write(pub) // nolint
tmpf.Close()
signer, err := sso.NewSigner(priv)
......@@ -101,7 +102,7 @@ func (c *testClient) request(uri string, req, out interface{}) error {
return json.Unmarshal(data, out)
}
func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backend, *testClient) {
func startServiceWithConfigAndCache(t testing.TB, svcConfig as.Config, enableCache bool) (func(), as.Backend, *testClient) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../ldaptest",
Port: testLDAPPort,
......@@ -114,10 +115,13 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe
},
})
be, err := backend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
be, err := ldapbackend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil {
t.Fatal("NewLDAPBackend", err)
}
if enableCache {
be = cachebackend.Wrap(be)
}
ssoStop, signer, ssoPubKeyFile := withSSO(t)
......@@ -163,6 +167,10 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe
}, be, c
}
func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backend, *testClient) {
return startServiceWithConfigAndCache(t, svcConfig, false)
}
func startService(t testing.TB) (func(), as.Backend, *testClient) {
return startServiceWithConfig(t, as.Config{})
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment