From a8c16f28539ddc361c0ea95fe2e7561742741ed6 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 2 Aug 2025 10:04:17 +0200 Subject: [PATCH] Add dedicated db cache table (#22650) --- cmd/cache-clear.go | 52 ++++++++++++++++++++++++++++ cmd/cache-get.go | 53 +++++++++++++++++++++++++++++ cmd/cache.go | 15 ++++++++ cmd/setup.go | 5 +++ i18n/de.json | 2 +- i18n/en.json | 2 +- server/db/cache/cache.go | 68 +++++++++++++++++++++++++++++++++++++ server/http_site_handler.go | 16 ++++----- tariff/proxy_cache.go | 6 ++-- 9 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 cmd/cache-clear.go create mode 100644 cmd/cache-get.go create mode 100644 cmd/cache.go create mode 100644 server/db/cache/cache.go diff --git a/cmd/cache-clear.go b/cmd/cache-clear.go new file mode 100644 index 000000000..84aedd22a --- /dev/null +++ b/cmd/cache-clear.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "github.com/AlecAivazis/survey/v2" + "github.com/evcc-io/evcc/server/db/cache" + "github.com/spf13/cobra" +) + +// cacheClearCmd represents the cache clear command +var cacheClearCmd = &cobra.Command{ + Use: "clear", + Short: "Clear all cache entries", + Run: runCacheClear, +} + +func init() { + cacheCmd.AddCommand(cacheClearCmd) + cacheClearCmd.Flags().BoolP(flagForce, "f", false, "Force (no confirmation)") +} + +func runCacheClear(cmd *cobra.Command, args []string) { + // load config + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { + log.FATAL.Fatal(err) + } + + // setup persistence + if err := configureDatabase(conf.Database); err != nil { + log.FATAL.Fatal(err) + } + + confirmation, _ := cmd.Flags().GetBool(flagForce) + if !confirmation { + prompt := &survey.Confirm{ + Message: "Clear all cache entries", + } + + if err := survey.AskOne(prompt, &confirmation); err != nil { + log.FATAL.Fatal(err) + } + } + + if confirmation { + if err := cache.Clear(); err != nil { + log.FATAL.Fatal(err) + } + log.INFO.Println("Cache cleared successfully") + } + + // wait for shutdown + <-shutdownDoneC() +} diff --git a/cmd/cache-get.go b/cmd/cache-get.go new file mode 100644 index 000000000..0d57d6a51 --- /dev/null +++ b/cmd/cache-get.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + "regexp" + + "github.com/evcc-io/evcc/server/db/cache" + "github.com/spf13/cobra" +) + +// cacheGetCmd represents the cache get command +var cacheGetCmd = &cobra.Command{ + Use: "get", + Short: "Get cache entries", + Run: runCacheGet, + Args: cobra.MaximumNArgs(1), +} + +func init() { + cacheCmd.AddCommand(cacheGetCmd) +} + +func runCacheGet(cmd *cobra.Command, args []string) { + // load config + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { + log.FATAL.Fatal(err) + } + + // setup persistence + if err := configureDatabase(conf.Database); err != nil { + log.FATAL.Fatal(err) + } + + var re *regexp.Regexp + if len(args) > 0 { + re = regexp.MustCompile(args[0]) + } + + entries, err := cache.All() + if err != nil { + log.FATAL.Fatal(err) + } + + for _, entry := range entries { + if re != nil && !re.MatchString(entry.Key) { + continue + } + + fmt.Printf("%s:\n", entry.Key) + fmt.Println(entry.Value) + fmt.Println("---") + } +} diff --git a/cmd/cache.go b/cmd/cache.go new file mode 100644 index 000000000..ce88aea89 --- /dev/null +++ b/cmd/cache.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// cacheCmd represents the cache command +var cacheCmd = &cobra.Command{ + Use: "cache", + Short: "Manage cache entries", +} + +func init() { + rootCmd.AddCommand(cacheCmd) +} diff --git a/cmd/setup.go b/cmd/setup.go index e2a4b7d06..f61dbeefe 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -33,6 +33,7 @@ import ( "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/server/db/cache" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/server/modbus" @@ -588,6 +589,10 @@ func configureDatabase(conf globalconfig.DB) error { return err } + if err := cache.Init(); err != nil { + return err + } + persistSettings := func() { if err := settings.Persist(); err != nil { log.ERROR.Println("cannot save settings:", err) diff --git a/i18n/de.json b/i18n/de.json index 331e60416..81d2de9a0 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -457,7 +457,7 @@ "sessions": "Ladevorgänge", "sessionsDescription": "Löscht den Verlauf deiner Ladevorgänge.", "settings": "Konfiguration & Einstellungen", - "settingsDescription": "Löscht alle konfigurierten Geräte, Dienste, Pläne, usw.", + "settingsDescription": "Löscht alle konfigurierten Geräte, Dienste, Pläne, Caches, usw.", "title": "Zurücksetzen" }, "restore": { diff --git a/i18n/en.json b/i18n/en.json index 709af452e..f470f6cd1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -457,7 +457,7 @@ "sessions": "Charging sessions", "sessionsDescription": "Deletes your charging session history.", "settings": "Configuration & settings", - "settingsDescription": "Deletes all configured devices, services, plans, etc.", + "settingsDescription": "Deletes all configured devices, services, plans, caches, etc.", "title": "Reset" }, "restore": { diff --git a/server/db/cache/cache.go b/server/db/cache/cache.go new file mode 100644 index 000000000..e4175621d --- /dev/null +++ b/server/db/cache/cache.go @@ -0,0 +1,68 @@ +package cache + +import ( + "cmp" + "encoding/json" + "errors" + "slices" + + "github.com/evcc-io/evcc/server/db" + "gorm.io/gorm" +) + +var ErrNotFound = errors.New("cache entry not found") + +type Cache struct { + Key string `json:"key" gorm:"primarykey"` + Value string `json:"value"` +} + +func Init() error { + return db.Instance.AutoMigrate(new(Cache)) +} + +func Put(key string, value interface{}) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + + return db.Instance.Save(&Cache{ + Key: key, + Value: string(data), + }).Error +} + +func Get(key string, result interface{}) error { + var cacheEntry Cache + + err := db.Instance.First(&cacheEntry, "key = ?", key).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrNotFound + } + return err + } + + return json.Unmarshal([]byte(cacheEntry.Value), result) +} + +func All() ([]Cache, error) { + var entries []Cache + + err := db.Instance.Find(&entries).Error + if err != nil { + return nil, err + } + + // Sort by key for consistent output + slices.SortFunc(entries, func(i, j Cache) int { + return cmp.Compare(i.Key, j.Key) + }) + + return entries, nil +} + +func Clear() error { + return db.Instance.Delete(&Cache{}, "1=1").Error +} diff --git a/server/http_site_handler.go b/server/http_site_handler.go index aa2eb8a9c..9b274b50d 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -501,15 +501,13 @@ func resetDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc { } if req.Settings { - query := db.Instance.Exec("DELETE FROM settings") - if query.Error != nil { - jsonError(w, http.StatusInternalServerError, query.Error) - return - } - query = db.Instance.Exec("DELETE FROM configs") - if query.Error != nil { - jsonError(w, http.StatusInternalServerError, query.Error) - return + tables := []string{"settings", "configs", "caches", "meters"} + + for _, table := range tables { + if err := db.Instance.Exec("DELETE FROM " + table).Error; err != nil { + jsonError(w, http.StatusInternalServerError, err) + return + } } } diff --git a/tariff/proxy_cache.go b/tariff/proxy_cache.go index bac9108ce..e01672fdc 100644 --- a/tariff/proxy_cache.go +++ b/tariff/proxy_cache.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/server/db/settings" + "github.com/evcc-io/evcc/server/db/cache" ) type cached struct { @@ -18,7 +18,7 @@ func cacheKey(typ string, other map[string]any) string { } func cachePut(key string, typ api.TariffType, rates api.Rates) error { - return settings.SetJson(key, &cached{ + return cache.Put(key, &cached{ Type: typ, Rates: rates, }) @@ -26,5 +26,5 @@ func cachePut(key string, typ api.TariffType, rates api.Rates) error { func cacheGet(key string) (*cached, error) { var res cached - return &res, settings.Json(key, &res) + return &res, cache.Get(key, &res) }