tariff/proxy_cache.go
Rama master · réplica de lectura del repositorio original.
package tariff
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"slices"
"sync"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/db/cache"
"github.com/evcc-io/evcc/util"
)
// defaultInterval is the update interval assumed if the tariff doesn't configure one
const defaultInterval = time.Hour
// cachingProxy wraps a tariff with caching
type cachingProxy struct {
mu sync.Mutex
hash [32]byte
key string
ctx context.Context
typ string
config map[string]any
interval time.Duration
updated time.Time
cached *cached
tariff api.Tariff
}
var _ api.Tariff = (*cachingProxy)(nil)
// NewCachedFromConfig creates a proxy that controls tariff instantiation and caching
func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any) (api.Tariff, error) {
tariffType := typ
if template := util.TemplateName(typ, other); template != "" {
tariffType = template
}
cc := struct {
Interval time.Duration
Other map[string]any `mapstructure:",remain"`
}{
Interval: defaultInterval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
p := &cachingProxy{
ctx: ctx,
typ: typ,
config: other,
interval: cc.Interval,
key: tariffType + "-" + cacheKey(typ, other),
}
log := util.NewLogger("tariff")
// check if cached data is up to date
if _, err := p.cacheGet(); err != nil {
// attempt to create a new instance
if err := p.createInstance(); err != nil {
// if no usable cached data available, return error
if !p.usableCache() {
return nil, err
}
// use outdated cached data until tariff becomes available
log.WARN.Printf("tariff not available, using cached rates (updated: %s): %v", p.cached.Updated.Local(), err)
}
}
if p.tariff == nil {
log.DEBUG.Printf("using cache: %s (updated: %s)", p.key, p.cached.Updated.Local())
}
return p, nil
}
// createInstance creates the tariff. If that fails, the wrapper takes its place.
func (p *cachingProxy) createInstance() error {
t, err := NewFromConfig(p.ctx, p.typ, p.config)
if err != nil {
t = NewWrapper(p.ctx, p.typ, p.config, err)
}
p.tariff = t
return err
}
// instance returns the tariff, creating it once cached data is outdated
func (p *cachingProxy) instance() api.Tariff {
if p.tariff == nil {
if _, err := p.cacheGet(); err != nil {
_ = p.createInstance()
}
}
return p.tariff
}
// Rates returns cached data until underlying tariff is created, then delegates to tariff.
// Cached data is served while the tariff is unavailable.
func (p *cachingProxy) Rates() (api.Rates, error) {
p.mu.Lock()
defer p.mu.Unlock()
t := p.instance()
if t == nil {
// cached data is up to date
return slices.Clone(p.cached.Rates), nil
}
res, err := t.Rates()
if err != nil {
if p.usableCache() {
return slices.Clone(p.cached.Rates), nil
}
return nil, err
}
// a fresh tariff starts with its first fetch, keep the cached slots leading up to it
if p.hasCache() {
res = mergeAfter(p.cached.Rates, res, time.Now().Truncate(SlotDuration))
}
if p.dynamicTariff() {
err = p.cachePut(t.Type(), res)
}
return res, err
}
// Type returns the tariff type
func (p *cachingProxy) Type() api.TariffType {
p.mu.Lock()
defer p.mu.Unlock()
if t := p.instance(); t != nil {
// wrapper reports unknown type while tariff is unavailable
if typ := t.Type(); typ != 0 {
return typ
}
}
if p.hasCache() {
return p.cached.Type
}
return 0 // unknown
}
func (p *cachingProxy) dynamicTariff() bool {
return slices.Contains([]api.TariffType{
api.TariffTypePriceForecast,
api.TariffTypeCo2,
api.TariffTypeSolar,
}, p.tariff.Type())
}
// hasCache returns true if cached rates are available, regardless of their age
func (p *cachingProxy) hasCache() bool {
return p.cached != nil && len(p.cached.Rates) > 0
}
// usableCache returns true if cached rates still reach into the future. Rates are sorted by start,
// so the last slot ends latest. Entirely elapsed rates cannot inform any decision.
func (p *cachingProxy) usableCache() bool {
return p.hasCache() && p.cached.Rates[len(p.cached.Rates)-1].End.After(time.Now())
}
// cacheGet returns cached data if the update interval has not yet elapsed
func (p *cachingProxy) cacheGet() (*cached, error) {
if p.cached == nil {
res, err := cacheGet(p.key)
if err != nil {
return nil, err
}
p.cached = res
}
if !p.usableCache() {
return nil, errors.New("no rates")
}
if d := time.Since(p.cached.Updated); d > p.interval {
return nil, fmt.Errorf("cache outdated: %v", d.Round(time.Second))
}
return p.cached, nil
}
// cachePut persists rates if changed or the update interval has elapsed
func (p *cachingProxy) cachePut(typ api.TariffType, rates api.Rates) error {
hash := sha256.Sum256(fmt.Append(nil, rates))
if hash == p.hash && time.Since(p.updated) < p.interval {
return nil
}
p.hash = hash
p.updated = time.Now()
p.cached = &cached{
Type: typ,
Rates: rates,
Updated: p.updated,
}
return cache.Put(p.key, p.cached)
}