mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Config: Add a simple cache to reduce disk I/O under stress
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
f23069dd2c
commit
3624e73d36
6 changed files with 62 additions and 19 deletions
|
|
@ -43,6 +43,7 @@ import (
|
|||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/pbnjay/memory"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
|
@ -82,6 +83,7 @@ type Config struct {
|
|||
env string
|
||||
start bool
|
||||
ready atomic.Bool
|
||||
cache *gc.Cache
|
||||
}
|
||||
|
||||
// Values is a shorthand alias for map[string]interface{}.
|
||||
|
|
@ -163,6 +165,7 @@ func NewConfig(ctx *cli.Context) *Config {
|
|||
token: rnd.Base36(8),
|
||||
env: os.Getenv("DOCKER_ENV"),
|
||||
start: start,
|
||||
cache: gc.New(time.Minute, 10*time.Minute),
|
||||
}
|
||||
|
||||
// Override options with values from the "options.yml" file, if it exists.
|
||||
|
|
|
|||
|
|
@ -131,33 +131,42 @@ func (c *Config) NodeThemeVersion() string {
|
|||
// lazily loads the token from disk (or generates a new one) and caches it in
|
||||
// memory. Example format: k9sEFe6-A7gt6zqm-gY9gFh0.
|
||||
func (c *Config) JoinToken() string {
|
||||
if s := strings.TrimSpace(c.options.JoinToken); rnd.IsJoinToken(s, false) {
|
||||
c.options.JoinToken = s
|
||||
return s
|
||||
// Read token from config options (memory).
|
||||
if rnd.IsJoinToken(c.options.JoinToken, false) {
|
||||
return c.options.JoinToken
|
||||
}
|
||||
|
||||
if fileName := c.JoinTokenFile(); fs.FileExistsNotEmpty(fileName) {
|
||||
if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
|
||||
log.Warnf("config: could not read cluster join token from %s (%s)", fileName, err)
|
||||
} else if s := strings.TrimSpace(string(b)); rnd.IsJoinToken(s, false) {
|
||||
c.options.JoinToken = s
|
||||
return s
|
||||
} else {
|
||||
log.Warnf("config: cluster join token from %s is shorter than %d characters", fileName, rnd.JoinTokenLength)
|
||||
// Read token from file if possible. Uses a cache to reduce I/O.
|
||||
if fileName := c.JoinTokenFile(); fileName != "" {
|
||||
if c.cache == nil {
|
||||
// Skip cache lookup.
|
||||
} else if s, hit := c.cache.Get(fileName); hit && s != nil {
|
||||
return s.(string)
|
||||
}
|
||||
|
||||
if fs.FileExistsNotEmpty(fileName) {
|
||||
if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
|
||||
log.Warnf("config: could not read cluster join token from %s (%s)", fileName, err)
|
||||
} else if s := strings.TrimSpace(string(b)); rnd.IsJoinToken(s, false) {
|
||||
if c.cache != nil {
|
||||
c.cache.SetDefault(fileName, s)
|
||||
}
|
||||
return s
|
||||
} else {
|
||||
log.Warnf("config: cluster join token from %s is shorter than %d characters", fileName, rnd.JoinTokenLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not proceed with generating a token on nodes.
|
||||
if !c.Portal() {
|
||||
return ""
|
||||
}
|
||||
|
||||
token, _, err := c.SaveJoinToken("")
|
||||
if err != nil {
|
||||
} else if token, _, err := c.SaveJoinToken(""); err != nil {
|
||||
log.Errorf("config: %v", err)
|
||||
return ""
|
||||
} else {
|
||||
return token
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// SaveJoinToken writes a fresh portal join token to disk and updates the
|
||||
|
|
@ -194,11 +203,20 @@ func (c *Config) SaveJoinToken(customToken string) (token string, fileName strin
|
|||
return "", "", fmt.Errorf("could not write cluster join token (%w)", err)
|
||||
}
|
||||
|
||||
c.options.JoinToken = token
|
||||
if c.cache != nil {
|
||||
c.cache.SetDefault(fileName, token)
|
||||
}
|
||||
|
||||
return token, fileName, nil
|
||||
}
|
||||
|
||||
// clearJoinTokenFileCache invalidates the cached join token file cache.
|
||||
func (c *Config) clearJoinTokenFileCache() {
|
||||
if c.cache != nil {
|
||||
c.cache.Delete(c.JoinTokenFile())
|
||||
}
|
||||
}
|
||||
|
||||
// JoinTokenFile returns the path where the portal join token is stored for the
|
||||
// active configuration (portal nodes use config/portal/secrets/join_token,
|
||||
// regular nodes use config/node/secrets/join_token).
|
||||
|
|
|
|||
|
|
@ -438,6 +438,13 @@ func TestConfig_Cluster(t *testing.T) {
|
|||
assert.Equal(t, cluster.ExampleClientSecret, c.NodeClientSecret())
|
||||
assert.Equal(t, cluster.ExampleJoinTokenAlt, c.JoinToken())
|
||||
|
||||
// Refreshing the token file should invalidate the cache.
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
newToken := cluster.ExampleJoinToken
|
||||
assert.NoError(t, os.WriteFile(tkFile, []byte(newToken), fs.ModeSecretFile))
|
||||
c.clearJoinTokenFileCache()
|
||||
assert.Equal(t, newToken, c.JoinToken())
|
||||
|
||||
// Empty / missing should yield empty strings.
|
||||
t.Setenv("PHOTOPRISM_NODE_CLIENT_SECRET_FILE", filepath.Join(dir, "missing"))
|
||||
t.Setenv("PHOTOPRISM_JOIN_TOKEN_FILE", filepath.Join(dir, "missing"))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package config
|
|||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
|
@ -233,6 +235,7 @@ func TestConfig_CreateDirectories(t *testing.T) {
|
|||
c := &Config{
|
||||
options: NewTestOptions("config"),
|
||||
token: rnd.Base36(8),
|
||||
cache: gc.New(time.Second, time.Minute),
|
||||
}
|
||||
|
||||
assert.NoError(t, c.CreateDirectories())
|
||||
|
|
@ -244,6 +247,7 @@ func TestConfig_CreateDirectories(t *testing.T) {
|
|||
c := &Config{
|
||||
options: NewTestOptions("config"),
|
||||
token: rnd.Base36(8),
|
||||
cache: gc.New(time.Second, time.Minute),
|
||||
}
|
||||
|
||||
c.options.StoragePath = "./testdata"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
|
|
@ -293,6 +294,7 @@ func NewIsolatedTestConfig(dbName, dataPath string, createDirs bool) *Config {
|
|||
c := &Config{
|
||||
options: opts,
|
||||
token: rnd.Base36(8),
|
||||
cache: gc.New(time.Second, time.Minute),
|
||||
}
|
||||
|
||||
if !createDirs {
|
||||
|
|
@ -318,6 +320,7 @@ func NewTestConfig(dbName string) *Config {
|
|||
cliCtx: CliTestContext(),
|
||||
options: NewTestOptions(dbName),
|
||||
token: rnd.Base36(8),
|
||||
cache: gc.New(time.Second, time.Minute),
|
||||
}
|
||||
|
||||
s := customize.NewSettings(c.DefaultTheme(), c.DefaultLocale(), c.DefaultTimezone().String())
|
||||
|
|
@ -351,7 +354,10 @@ func NewTestConfig(dbName string) *Config {
|
|||
|
||||
// NewTestErrorConfig returns an invalid test config.
|
||||
func NewTestErrorConfig() *Config {
|
||||
c := &Config{options: NewTestOptionsError()}
|
||||
c := &Config{
|
||||
options: NewTestOptionsError(),
|
||||
cache: gc.New(time.Second, time.Minute),
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,11 @@ func isJoinTokenSeparatorIndex(i int) bool {
|
|||
|
||||
// IsJoinToken checks if the string represents a join token.
|
||||
func IsJoinToken(s string, strict bool) bool {
|
||||
// Basic mode: No token, not valid.
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Non-strict mode: only enforce minimum length so legacy tokens that were
|
||||
// longer than the auto-generated format continue to work.
|
||||
if !strict {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue