Ctrl K
ComunidadGuías…

tariff/proxy_cache.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}