81b0d94ba3
- Introduced a new `widgetCache` structure to manage caching for various dashboard widgets, improving performance and reducing redundant data retrieval. - Updated the `ClosingSoon` and `Trend` methods in the service to utilize the new caching mechanism, ensuring efficient data access and reducing load on the database. - Enhanced the `NoticeTypes` and `Countries` methods to implement caching, further optimizing the dashboard's responsiveness. - Added a new `searchListCache` implementation in the tender service to cache search results, improving the efficiency of tender searches and reducing database load. - Implemented robust error handling and logging for cache operations, ensuring better visibility into potential issues. This update significantly enhances the performance and scalability of the dashboard and tender services by integrating effective caching strategies, leading to improved user experience and resource management.
225 lines
4.7 KiB
Go
225 lines
4.7 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/redis"
|
|
|
|
goredis "github.com/redis/go-redis/v9"
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
const (
|
|
widgetCacheTTL = 60 * time.Second
|
|
widgetStaleGraceTTL = 5 * time.Minute
|
|
widgetReloadTimeout = 2 * time.Minute
|
|
)
|
|
|
|
type widgetLoader[T any] func(ctx context.Context) (T, error)
|
|
|
|
type widgetCache[T any] struct {
|
|
mu sync.Mutex
|
|
entries map[string]cacheEntry[T]
|
|
group singleflight.Group
|
|
redis redis.Client
|
|
logger logger.Logger
|
|
keyPrefix string
|
|
ttl time.Duration
|
|
staleGrace time.Duration
|
|
}
|
|
|
|
func newWidgetCache[T any](redisClient redis.Client, log logger.Logger, keyPrefix string) *widgetCache[T] {
|
|
return &widgetCache[T]{
|
|
entries: make(map[string]cacheEntry[T]),
|
|
redis: redisClient,
|
|
logger: log,
|
|
keyPrefix: keyPrefix,
|
|
ttl: widgetCacheTTL,
|
|
staleGrace: widgetStaleGraceTTL,
|
|
}
|
|
}
|
|
|
|
func (c *widgetCache[T]) Get(ctx context.Context, key string, load widgetLoader[T]) (T, error) {
|
|
var zero T
|
|
|
|
if value, ok := c.fresh(key); ok {
|
|
return value, nil
|
|
}
|
|
if value, ok := c.stale(key); ok {
|
|
go c.Reload(key, load)
|
|
return value, nil
|
|
}
|
|
if value, ok := c.fromRedis(ctx, key); ok {
|
|
c.store(key, value)
|
|
go c.Reload(key, load)
|
|
return value, nil
|
|
}
|
|
|
|
out, err, _ := c.group.Do(key, func() (interface{}, error) {
|
|
if value, ok := c.fresh(key); ok {
|
|
return value, nil
|
|
}
|
|
|
|
result, err := load(context.WithoutCancel(ctx))
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
c.store(key, result)
|
|
c.storeRedis(context.Background(), key, result)
|
|
return result, nil
|
|
})
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
return out.(T), nil
|
|
}
|
|
|
|
func (c *widgetCache[T]) Reload(key string, load widgetLoader[T]) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), widgetReloadTimeout)
|
|
defer cancel()
|
|
|
|
_, _, _ = c.group.Do("reload:"+key, func() (interface{}, error) {
|
|
result, err := load(ctx)
|
|
if err != nil {
|
|
if c.logger != nil {
|
|
c.logger.Warn("Failed to refresh dashboard widget cache", map[string]interface{}{
|
|
"widget": c.keyPrefix,
|
|
"key": key,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
c.store(key, result)
|
|
c.storeRedis(context.Background(), key, result)
|
|
return result, nil
|
|
})
|
|
}
|
|
|
|
func (c *widgetCache[T]) WarmFromRedis(ctx context.Context, key string) bool {
|
|
value, ok := c.fromRedis(ctx, key)
|
|
if !ok {
|
|
return false
|
|
}
|
|
c.store(key, value)
|
|
return true
|
|
}
|
|
|
|
func (c *widgetCache[T]) fresh(key string) (T, bool) {
|
|
var zero T
|
|
now := time.Now()
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
entry, ok := c.entries[key]
|
|
if !ok || now.After(entry.expiresAt) {
|
|
return zero, false
|
|
}
|
|
|
|
return entry.value, true
|
|
}
|
|
|
|
func (c *widgetCache[T]) stale(key string) (T, bool) {
|
|
var zero T
|
|
now := time.Now()
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
entry, ok := c.entries[key]
|
|
if !ok || now.After(entry.staleUntil) {
|
|
if ok {
|
|
delete(c.entries, key)
|
|
}
|
|
return zero, false
|
|
}
|
|
|
|
return entry.value, true
|
|
}
|
|
|
|
func (c *widgetCache[T]) store(key string, value T) {
|
|
now := time.Now()
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.entries[key] = cacheEntry[T]{
|
|
expiresAt: now.Add(c.ttl),
|
|
staleUntil: now.Add(c.ttl + c.staleGrace),
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
func (c *widgetCache[T]) redisKey(key string) string {
|
|
return fmt.Sprintf("dashboard:%s:%s", c.keyPrefix, key)
|
|
}
|
|
|
|
func (c *widgetCache[T]) fromRedis(ctx context.Context, key string) (T, bool) {
|
|
var zero T
|
|
if c.redis == nil {
|
|
return zero, false
|
|
}
|
|
|
|
raw, err := c.redis.Get(ctx, c.redisKey(key))
|
|
if err != nil {
|
|
if err != goredis.Nil && c.logger != nil {
|
|
c.logger.Warn("Failed to read dashboard widget cache from Redis", map[string]interface{}{
|
|
"widget": c.keyPrefix,
|
|
"key": key,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
return zero, false
|
|
}
|
|
|
|
var value T
|
|
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
|
if c.logger != nil {
|
|
c.logger.Warn("Failed to decode dashboard widget cache from Redis", map[string]interface{}{
|
|
"widget": c.keyPrefix,
|
|
"key": key,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
_ = c.redis.Del(ctx, c.redisKey(key))
|
|
return zero, false
|
|
}
|
|
|
|
return value, true
|
|
}
|
|
|
|
func (c *widgetCache[T]) storeRedis(ctx context.Context, key string, value T) {
|
|
if c.redis == nil {
|
|
return
|
|
}
|
|
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
if c.logger != nil {
|
|
c.logger.Warn("Failed to encode dashboard widget cache for Redis", map[string]interface{}{
|
|
"widget": c.keyPrefix,
|
|
"key": key,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
ttl := c.ttl + c.staleGrace
|
|
if err := c.redis.Set(ctx, c.redisKey(key), string(encoded), ttl); err != nil && c.logger != nil {
|
|
c.logger.Warn("Failed to store dashboard widget cache in Redis", map[string]interface{}{
|
|
"widget": c.keyPrefix,
|
|
"key": key,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|