@@ -3,11 +3,13 @@ package repository
33import (
44 "context"
55 "encoding/json"
6+ "fmt"
7+ "stellarbill-backend/internal/cache"
68 "sync"
79 "sync/atomic"
810 "time"
911
10- "stellarbill-backend/internal/cache "
12+ "golang.org/x/sync/singleflight "
1113)
1214
1315type cacheEnvelope struct {
@@ -25,6 +27,7 @@ type CachedPlanRepo struct {
2527 misses uint64
2628 stales uint64
2729 invalidatedAt sync.Map
30+ sf singleflight.Group
2831}
2932
3033// NewCachedPlanRepo constructs a CachedPlanRepo.
@@ -44,10 +47,12 @@ func (cpr *CachedPlanRepo) cacheKey(id string) string {
4447// and updates cache on a successful backend read.
4548func (cpr * CachedPlanRepo ) FindByID (ctx context.Context , id string ) (* PlanRow , error ) {
4649 key := cpr .cacheKey (id )
50+ // Attempt cache fetch
4751 if cpr .cache != nil {
4852 if val , err := cpr .cache .Get (ctx , key ); err == nil && val != nil {
4953 var env cacheEnvelope
5054 if err := json .Unmarshal (val , & env ); err == nil {
55+ // Check for staleness due to invalidation
5156 stale := false
5257 if invTimeVal , ok := cpr .invalidatedAt .Load (key ); ok {
5358 if invTime , ok := invTimeVal .(time.Time ); ok && env .StoredAt .Before (invTime ) {
@@ -67,29 +72,34 @@ func (cpr *CachedPlanRepo) FindByID(ctx context.Context, id string) (*PlanRow, e
6772 }
6873 }
6974 }
75+ // Cache miss, use singleflight to avoid stampede
7076 atomic .AddUint64 (& cpr .misses , 1 )
71- pr , err := cpr .backend .FindByID (ctx , id )
72- if err != nil {
73- return nil , err
74- }
75- if cpr .cache != nil {
76- prBytes , err := json .Marshal (pr )
77- if err == nil {
78- env := cacheEnvelope {
79- Data : prBytes ,
80- StoredAt : time .Now (),
81- }
82- if envBytes , err := json .Marshal (env ); err == nil {
83- _ = cpr .cache .Set (ctx , key , envBytes , cpr .ttl )
77+ v , err , _ := cpr .sf .Do (key , func () (interface {}, error ) {
78+ pr , err := cpr .backend .FindByID (ctx , id )
79+ if err != nil {
80+ return nil , err
81+ }
82+ if cpr .cache != nil {
83+ prBytes , err := json .Marshal (pr )
84+ if err == nil {
85+ env := cacheEnvelope {Data : prBytes , StoredAt : time .Now ()}
86+ if envBytes , err := json .Marshal (env ); err == nil {
87+ _ = cpr .cache .Set (ctx , key , envBytes , cpr .ttl )
88+ }
8489 }
8590 }
91+ return pr , nil
92+ })
93+ if err != nil {
94+ return nil , err
8695 }
87- return pr , nil
96+ return v .( * PlanRow ) , nil
8897}
8998
9099// List returns all plans. It caches the full list under a single key.
91100func (cpr * CachedPlanRepo ) List (ctx context.Context ) ([]* PlanRow , error ) {
92101 key := "plan:list:all"
102+ // Attempt cache fetch for list
93103 if cpr .cache != nil {
94104 if val , err := cpr .cache .Get (ctx , key ); err == nil && val != nil {
95105 var env cacheEnvelope
@@ -110,27 +120,34 @@ func (cpr *CachedPlanRepo) List(ctx context.Context) ([]*PlanRow, error) {
110120 return out , nil
111121 }
112122 }
123+ } else {
124+ // Corrupted envelope JSON
125+ return nil , fmt .Errorf ("corrupted cache envelope: %w" , err )
113126 }
114127 }
115128 }
129+ // Cache miss, use singleflight for list
116130 atomic .AddUint64 (& cpr .misses , 1 )
117- out , err := cpr .backend .List (ctx )
118- if err != nil {
119- return nil , err
120- }
121- if cpr .cache != nil {
122- outBytes , err := json .Marshal (out )
123- if err == nil {
124- env := cacheEnvelope {
125- Data : outBytes ,
126- StoredAt : time .Now (),
127- }
128- if envBytes , err := json .Marshal (env ); err == nil {
129- _ = cpr .cache .Set (ctx , key , envBytes , cpr .ttl )
131+ v , err , _ := cpr .sf .Do (key , func () (interface {}, error ) {
132+ out , err := cpr .backend .List (ctx )
133+ if err != nil {
134+ return nil , err
135+ }
136+ if cpr .cache != nil {
137+ outBytes , err := json .Marshal (out )
138+ if err == nil {
139+ env := cacheEnvelope {Data : outBytes , StoredAt : time .Now ()}
140+ if envBytes , err := json .Marshal (env ); err == nil {
141+ _ = cpr .cache .Set (ctx , key , envBytes , cpr .ttl )
142+ }
130143 }
131144 }
145+ return out , nil
146+ })
147+ if err != nil {
148+ return nil , err
132149 }
133- return out , nil
150+ return v .([] * PlanRow ) , nil
134151}
135152
136153// Delete invalidates a cached plan entry and records the invalidation time.
0 commit comments