-
Notifications
You must be signed in to change notification settings - Fork 20
ECOPROJECT-4359 | feat: Add metrics cache to reduce database queries for inventory statistics #1180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package store | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "sync/atomic" | ||
| "time" | ||
|
|
||
| "github.com/kubev2v/migration-planner/internal/store/model" | ||
| "golang.org/x/sync/singleflight" | ||
| ) | ||
|
|
||
| const ( | ||
| minCooldownPeriod = 5 * time.Minute | ||
| maxCooldownPeriod = 3 * time.Hour | ||
| ) | ||
|
|
||
| // MetricsCache manages cached inventory statistics | ||
| type MetricsCache struct { | ||
| assessmentStore Assessment | ||
| needsUpdate atomic.Bool // Set when the server modifies data requiring cache refresh | ||
|
|
||
| stats atomic.Pointer[model.InventoryStats] | ||
| lastRefresh atomic.Int64 | ||
|
|
||
| group singleflight.Group | ||
| } | ||
|
|
||
| // NewMetricsCache creates a new metrics cache | ||
| func NewMetricsCache(s Assessment) *MetricsCache { | ||
| return &MetricsCache{ | ||
| assessmentStore: s, | ||
| } | ||
| } | ||
|
|
||
| // GetStats returns cached stats, refreshing only if cooldown expired. | ||
| func (mc *MetricsCache) GetStats(ctx context.Context) (model.InventoryStats, error) { | ||
| ptr := mc.stats.Load() | ||
|
|
||
| if ptr != nil && !mc.shouldRefresh() { | ||
| return *ptr, nil | ||
| } | ||
|
|
||
| v, err, _ := mc.group.Do("refresh_stats", func() (any, error) { | ||
|
|
||
| assessments, err := mc.assessmentStore.List(ctx, NewAssessmentQueryFilter()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| stats := model.NewInventoryStats(assessments) | ||
|
|
||
| mc.stats.Store(&stats) | ||
| mc.lastRefresh.Store(time.Now().UnixNano()) | ||
| mc.needsUpdate.Store(false) | ||
|
|
||
| return stats, nil | ||
| }) | ||
|
|
||
| if err != nil { | ||
| return model.InventoryStats{}, fmt.Errorf("refresh cache failed: %w", err) | ||
| } | ||
|
|
||
| return v.(model.InventoryStats), nil | ||
| } | ||
|
AvielSegev marked this conversation as resolved.
|
||
|
|
||
| func (mc *MetricsCache) RequestMetricsCacheRefresh() { | ||
| mc.needsUpdate.Store(true) | ||
| } | ||
|
|
||
| // shouldRefresh checks if cooldown period has passed | ||
| func (mc *MetricsCache) shouldRefresh() bool { | ||
| last := mc.lastRefresh.Load() | ||
| if last == 0 { | ||
| return true | ||
| } | ||
|
|
||
| // Potential change by other pods | ||
| if time.Since(time.Unix(0, last)) > maxCooldownPeriod { | ||
| return true | ||
| } | ||
|
Comment on lines
+78
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify distributed cache invalidation limitations. The comment "Potential change by other pods" is misleading. This implementation has no distributed cache invalidation—each pod maintains independent cache state. When pod A modifies an assessment, pod B's cache won't refresh until If this trade-off is intentional (freshness vs. performance), consider clarifying the comment and documenting the staleness window in multi-pod deployments. 🤖 Prompt for AI Agents |
||
|
|
||
| if !mc.needsUpdate.Load() { | ||
| return false | ||
| } | ||
|
|
||
| return time.Since(time.Unix(0, last)) > minCooldownPeriod | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ type Store interface { | |
| PartnerCustomer() PartnerCustomer | ||
| Statistics(ctx context.Context) (model.InventoryStats, error) | ||
| Close() error | ||
| RequestMetricsCacheRefresh() | ||
| } | ||
|
|
||
| type DataStore struct { | ||
|
|
@@ -37,21 +38,25 @@ type DataStore struct { | |
| job Job | ||
| accounts Accounts | ||
| partnerCustomer PartnerCustomer | ||
| metricCache *MetricsCache | ||
| } | ||
|
|
||
| func NewStore(db *gorm.DB) Store { | ||
| assessment := NewAssessmentStore(db) | ||
|
|
||
| return &DataStore{ | ||
| agent: NewAgentSource(db), | ||
| source: NewSource(db), | ||
| imageInfra: NewImageInfraStore(db), | ||
| privateKey: NewCacheKeyStore(NewPrivateKey(db)), | ||
| label: NewLabelStore(db), | ||
| assessment: NewAssessmentStore(db), | ||
| assessment: assessment, | ||
| cluster: NewClusterSizingInputStore(db), | ||
| job: NewJobStore(db), | ||
| authz: NewAuthzStore(db), | ||
| accounts: NewAccountsStore(db), | ||
| partnerCustomer: NewPartnerCustomerStore(db), | ||
| metricCache: NewMetricsCache(assessment), | ||
| db: db, | ||
| } | ||
| } | ||
|
|
@@ -105,11 +110,11 @@ func (s *DataStore) PartnerCustomer() PartnerCustomer { | |
| } | ||
|
|
||
| func (s *DataStore) Statistics(ctx context.Context) (model.InventoryStats, error) { | ||
| assessments, err := s.Assessment().List(ctx, NewAssessmentQueryFilter()) | ||
| if err != nil { | ||
| return model.InventoryStats{}, err | ||
| } | ||
| return model.NewInventoryStats(assessments), nil | ||
| return s.metricCache.GetStats(ctx) | ||
| } | ||
|
Comment on lines
112
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff Consider fallback when cache refresh fails. When This depends on your availability vs. freshness requirements. If stale stats are acceptable during transient failures, consider:
If failing fast is correct (e.g., stats must be fresh), document this behavior. 🤖 Prompt for AI Agents |
||
|
|
||
| func (s *DataStore) RequestMetricsCacheRefresh() { | ||
| s.metricCache.RequestMetricsCacheRefresh() | ||
| } | ||
|
|
||
| func (s *DataStore) Close() error { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.