Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions internal/bootstrap/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (

"github.com/OpenListTeam/OpenList/v4/cmd/flags"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/cache"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/mem"
"github.com/OpenListTeam/OpenList/v4/internal/net"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/caarlos0/env/v9"
"github.com/shirou/gopsutil/v4/mem"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -96,22 +97,34 @@ func InitConfig() {
if !conf.Conf.Force {
confFromEnv()
}
cachePolicy, policyErr := cache.ResolvePolicy(conf.Conf.CachePolicy, cache.PolicyAuto)
if policyErr != nil {
log.Fatalf("resolve cache policy error: %+v", policyErr)
}
conf.Conf.CachePolicy = cachePolicy
conf.CachePolicy = cachePolicy
log.Infof("cache policy: %s", conf.CachePolicy)

if conf.Conf.MaxConcurrency > math.MaxInt32 {
net.DefaultConcurrencyLimit = &net.ConcurrencyLimit{Limit: math.MaxInt32}
} else if conf.Conf.MaxConcurrency > 0 {
net.DefaultConcurrencyLimit = &net.ConcurrencyLimit{Limit: uint32(conf.Conf.MaxConcurrency)}
}

memStat, _ := mem.VirtualMemory()
if memStat != nil {
log.Infof("total memory: %dMB, available: %dMB", memStat.Total>>20, memStat.Available>>20)
memStat, memErr := mem.GetMemorySnapshot()
if memErr != nil {
log.Warnf("memory detection warning: %v", memErr)
}
memoryCacheEnabled := false
if memStat.Limit > 0 {
log.Infof("effective memory: limit=%dMB, used=%dMB, available=%dMB, source=%s", memStat.Limit>>20, memStat.Used>>20, memStat.Available>>20, memStat.Source)
if conf.Conf.MinFreeMemory < 0 {
conf.MinFreeMemory = 0
log.Info("disable memory cache")
} else {
memoryCacheEnabled = true
if conf.Conf.MinFreeMemory < 16 {
t := (memStat.Total >> 20) / 10
t := (memStat.Limit >> 20) / 10
conf.MinFreeMemory = max(16, min(t, 1024)) << 20
} else {
conf.MinFreeMemory = uint64(conf.Conf.MinFreeMemory) << 20
Expand All @@ -120,7 +133,7 @@ func InitConfig() {
}

if conf.Conf.MaxBlockLimit < 4 {
t := (memStat.Total >> 20) * 3 / 100
t := (memStat.Limit >> 20) * 3 / 100
conf.MaxBlockLimit = max(4, min(uint64(t), 64)) << 20
} else {
conf.MaxBlockLimit = uint64(conf.Conf.MaxBlockLimit) << 20
Expand All @@ -130,6 +143,9 @@ func InitConfig() {
conf.MinFreeMemory = 0
log.Warn("failed to get memory info, disable memory cache")
}
budgetCapacity := cacheMemoryBudgetCapacity(memStat.Available, conf.MinFreeMemory, memoryCacheEnabled)
mem.CacheMemoryBudget.SetCapacity(budgetCapacity)
log.Infof("cache memory budget: %dMB", budgetCapacity>>20)

if conf.Conf.AutoMemoryLimit > 0 {
conf.AutoMemoryLimit = uint64(conf.Conf.AutoMemoryLimit) << 20
Expand Down Expand Up @@ -169,6 +185,13 @@ func InitConfig() {
initURL()
}

func cacheMemoryBudgetCapacity(available, minFree uint64, enabled bool) uint64 {
if !enabled || available <= minFree {
return 0
}
return available - minFree
}

func confFromEnv() {
prefix := "OPENLIST_"
if flags.NoPrefix {
Expand Down
25 changes: 25 additions & 0 deletions internal/bootstrap/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package bootstrap

import "testing"

func TestCacheMemoryBudgetCapacity(t *testing.T) {
tests := []struct {
name string
available uint64
minFree uint64
enabled bool
want uint64
}{
{name: "enabled", available: 10, minFree: 3, enabled: true, want: 7},
{name: "disabled", available: 10, minFree: 3, want: 0},
{name: "reserve equals available", available: 10, minFree: 10, enabled: true, want: 0},
{name: "reserve exceeds available", available: 10, minFree: 11, enabled: true, want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cacheMemoryBudgetCapacity(tt.available, tt.minFree, tt.enabled); got != tt.want {
t.Fatalf("cacheMemoryBudgetCapacity() = %d, want %d", got, tt.want)
}
})
}
}
70 changes: 70 additions & 0 deletions internal/cache/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cache

import (
"fmt"
"strings"
)

type Policy string

const (
// PolicyInherit is only used by per-instance overrides. It is not a
// user-selectable cache policy.
PolicyInherit Policy = ""
PolicyAuto Policy = "auto"
PolicyMemory Policy = "memory"
PolicyDisk Policy = "disk"
)

// ParsePolicy parses a user-facing global policy, where an empty value means
// auto. Text unmarshalling keeps an empty per-instance value as inherit.
func ParsePolicy(value string) (Policy, error) {
policy := Policy(strings.ToLower(strings.TrimSpace(value)))
if policy == PolicyInherit {
return PolicyAuto, nil
}
if !policy.IsConcrete() {
return PolicyInherit, fmt.Errorf("invalid cache policy %q: expected auto, memory, or disk", value)
}
return policy, nil
}

func ResolvePolicy(override, fallback Policy) (Policy, error) {
policy := override
if policy == PolicyInherit {
policy = fallback
}
if !policy.IsConcrete() {
return PolicyInherit, fmt.Errorf("invalid cache policy %q: expected auto, memory, or disk", policy)
}
return policy, nil
}

func (p Policy) IsConcrete() bool {
return p == PolicyAuto || p == PolicyMemory || p == PolicyDisk
}

func (p Policy) MarshalText() ([]byte, error) {
if p == PolicyInherit {
return []byte{}, nil
}
if !p.IsConcrete() {
return nil, fmt.Errorf("invalid cache policy %q", p)
}
return []byte(p), nil
}

func (p *Policy) UnmarshalText(text []byte) error {
if strings.TrimSpace(string(text)) == "" {
// Preserve inherit for per-instance configs. Global configuration
// resolves an empty policy to auto during bootstrap.
*p = PolicyInherit
return nil
}
policy, err := ParsePolicy(string(text))
if err != nil {
return err
}
*p = policy
return nil
}
95 changes: 95 additions & 0 deletions internal/cache/policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cache

import (
"encoding/json"
"testing"

"github.com/caarlos0/env/v9"
)

func TestParsePolicy(t *testing.T) {
tests := []struct {
input string
want Policy
}{
{"", PolicyAuto},
{"auto", PolicyAuto},
{" MEMORY ", PolicyMemory},
{"Disk", PolicyDisk},
}
for _, tt := range tests {
got, err := ParsePolicy(tt.input)
if err != nil {
t.Fatalf("ParsePolicy(%q) error = %v", tt.input, err)
}
if got != tt.want {
t.Errorf("ParsePolicy(%q) = %q, want %q", tt.input, got, tt.want)
}
}
if _, err := ParsePolicy("hybrid"); err == nil {
t.Fatal("ParsePolicy() expected an error for an invalid policy")
}
}

func TestPolicyEnvironment(t *testing.T) {
t.Setenv("OPENLIST_TEST_CACHE_POLICY", " Disk ")
var cfg struct {
Policy Policy `env:"CACHE_POLICY"`
}
if err := env.ParseWithOptions(&cfg, env.Options{Prefix: "OPENLIST_TEST_"}); err != nil {
t.Fatalf("env.ParseWithOptions() error = %v", err)
}
if cfg.Policy != PolicyDisk {
t.Fatalf("environment policy = %q, want disk", cfg.Policy)
}
}

func TestResolvePolicy(t *testing.T) {
got, err := ResolvePolicy(PolicyInherit, PolicyDisk)
if err != nil || got != PolicyDisk {
t.Fatalf("ResolvePolicy(inherit, disk) = %q, %v", got, err)
}
got, err = ResolvePolicy(PolicyMemory, PolicyDisk)
if err != nil || got != PolicyMemory {
t.Fatalf("ResolvePolicy(memory, disk) = %q, %v", got, err)
}
if _, err := ResolvePolicy(PolicyInherit, Policy("invalid")); err == nil {
t.Fatal("ResolvePolicy() expected an error for an invalid fallback")
}
}

func TestPolicyJSON(t *testing.T) {
type config struct {
Policy Policy `json:"cache_policy"`
}
var cfg config
if err := json.Unmarshal([]byte(`{"cache_policy":" MEMORY "}`), &cfg); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if cfg.Policy != PolicyMemory {
t.Fatalf("json.Unmarshal() policy = %q, want memory", cfg.Policy)
}
b, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
if string(b) != `{"cache_policy":"memory"}` {
t.Fatalf("json.Marshal() = %s", b)
}
if err := json.Unmarshal([]byte(`{"cache_policy":"invalid"}`), &cfg); err == nil {
t.Fatal("json.Unmarshal() expected an error for an invalid policy")
}

cfg.Policy = PolicyInherit
b, err = json.Marshal(cfg)
if err != nil {
t.Fatalf("json.Marshal(inherit) error = %v", err)
}
var roundTrip config
if err := json.Unmarshal(b, &roundTrip); err != nil {
t.Fatalf("json.Unmarshal(inherit) error = %v", err)
}
if roundTrip.Policy != PolicyInherit {
t.Fatalf("inherit round trip policy = %q, want inherit", roundTrip.Policy)
}
}
57 changes: 30 additions & 27 deletions internal/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package conf
import (
"path/filepath"

"github.com/OpenListTeam/OpenList/v4/internal/cache"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
)

Expand Down Expand Up @@ -112,33 +113,34 @@ type MCP struct {
}

type Config struct {
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database" envPrefix:"DB_"`
Meilisearch Meilisearch `json:"meilisearch" envPrefix:"MEILISEARCH_"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
DistDir string `json:"dist_dir"`
Log LogConfig `json:"log" envPrefix:"LOG_"`
DelayedStart int `json:"delayed_start" env:"DELAYED_START"`
AutoMemoryLimit int `json:"auto_memory_limit" env:"AUTO_MEMORY_LIMIT"`
MinFreeMemory int `json:"min_free_memory" env:"MIN_FREE_MEMORY"`
MaxBlockLimit int `json:"max_block_limit" env:"MAX_BLOCK_LIMIT"`
MaxConnections int `json:"max_connections" env:"MAX_CONNECTIONS"`
MaxConcurrency int `json:"max_concurrency" env:"MAX_CONCURRENCY"`
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
Cors Cors `json:"cors" envPrefix:"CORS_"`
S3 S3 `json:"s3" envPrefix:"S3_"`
FTP FTP `json:"ftp" envPrefix:"FTP_"`
SFTP SFTP `json:"sftp" envPrefix:"SFTP_"`
MCP MCP `json:"mcp" envPrefix:"MCP_"`
LastLaunchedVersion string `json:"last_launched_version"`
ProxyAddress string `json:"proxy_address" env:"PROXY_ADDRESS"`
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database" envPrefix:"DB_"`
Meilisearch Meilisearch `json:"meilisearch" envPrefix:"MEILISEARCH_"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
DistDir string `json:"dist_dir"`
Log LogConfig `json:"log" envPrefix:"LOG_"`
DelayedStart int `json:"delayed_start" env:"DELAYED_START"`
CachePolicy cache.Policy `json:"cache_policy" env:"CACHE_POLICY"`
AutoMemoryLimit int `json:"auto_memory_limit" env:"AUTO_MEMORY_LIMIT"`
MinFreeMemory int `json:"min_free_memory" env:"MIN_FREE_MEMORY"`
MaxBlockLimit int `json:"max_block_limit" env:"MAX_BLOCK_LIMIT"`
MaxConnections int `json:"max_connections" env:"MAX_CONNECTIONS"`
MaxConcurrency int `json:"max_concurrency" env:"MAX_CONCURRENCY"`
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
Cors Cors `json:"cors" envPrefix:"CORS_"`
S3 S3 `json:"s3" envPrefix:"S3_"`
FTP FTP `json:"ftp" envPrefix:"FTP_"`
SFTP SFTP `json:"sftp" envPrefix:"SFTP_"`
MCP MCP `json:"mcp" envPrefix:"MCP_"`
LastLaunchedVersion string `json:"last_launched_version"`
ProxyAddress string `json:"proxy_address" env:"PROXY_ADDRESS"`
}

func DefaultConfig(dataDir string) *Config {
Expand Down Expand Up @@ -185,6 +187,7 @@ func DefaultConfig(dataDir string) *Config {
},
},
},
CachePolicy: cache.PolicyAuto,
AutoMemoryLimit: 4,
MaxConnections: 0,
MaxConcurrency: 64,
Expand Down
21 changes: 21 additions & 0 deletions internal/conf/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package conf

import (
"encoding/json"
"testing"

"github.com/OpenListTeam/OpenList/v4/internal/cache"
)

func TestDefaultCachePolicy(t *testing.T) {
cfg := DefaultConfig(t.TempDir())
if got := cfg.CachePolicy; got != cache.PolicyAuto {
t.Fatalf("default cache policy = %q, want auto", got)
}
if err := json.Unmarshal([]byte(`{}`), cfg); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if got := cfg.CachePolicy; got != cache.PolicyAuto {
t.Fatalf("cache policy after loading an old config = %q, want auto", got)
}
}
Loading