@@ -3,6 +3,7 @@ package inventory
33import (
44 "context"
55 "fmt"
6+ "maps"
67 "os"
78 "slices"
89 "sync"
@@ -35,6 +36,14 @@ type FeatureRule struct {
3536 predicate FeaturePredicate
3637}
3738
39+ func (r FeatureRule ) clone () FeatureRule {
40+ return FeatureRule {
41+ features : slices .Clone (r .features ),
42+ featureSet : maps .Clone (r .featureSet ),
43+ predicate : r .predicate ,
44+ }
45+ }
46+
3847// NewFeatureRule creates an availability rule over the supplied feature flags.
3948func NewFeatureRule (features []FeatureFlag , predicate FeaturePredicate ) FeatureRule {
4049 declared := make ([]FeatureFlag , 0 , len (features ))
@@ -124,22 +133,25 @@ type featureState struct {
124133
125134 mu sync.Mutex
126135 results map [FeatureFlag ]* featureResult
136+ waiting map [FeatureFlag ]map [FeatureFlag ]int
127137}
128138
129139type featureResult struct {
130140 ready chan struct {}
131141 enabled bool
142+ failed bool
143+ done bool
132144}
133145
134146type resolvingFeature struct {
135- flag FeatureFlag
136- parent * resolvingFeature
147+ flag FeatureFlag
137148}
138149
139150func newFeatureState (checker FeatureFlagChecker ) * featureState {
140151 return & featureState {
141152 checker : checker ,
142153 results : make (map [FeatureFlag ]* featureResult ),
154+ waiting : make (map [FeatureFlag ]map [FeatureFlag ]int ),
143155 }
144156}
145157
@@ -148,21 +160,34 @@ func (s *featureState) enabled(ctx context.Context, feature FeatureFlag) bool {
148160 return false
149161 }
150162
151- for current := resolvingFeatureFromContext (ctx ); current != nil ; current = current .parent {
152- if current .flag == feature {
153- fmt .Fprintf (os .Stderr , "Feature flag resolution cycle detected for %q\n " , feature )
154- return false
155- }
156- }
157-
163+ owner := resolvingFeatureFromContext (ctx )
158164 s .mu .Lock ()
159165 result , found := s .results [feature ]
166+ if found && result .done {
167+ enabled := result .enabled
168+ s .mu .Unlock ()
169+ return enabled
170+ }
160171 if ! found {
161172 result = & featureResult {ready : make (chan struct {})}
162173 s .results [feature ] = result
163174 }
175+ if owner != nil {
176+ if path := s .pathLocked (feature , owner .flag , nil ); path != nil {
177+ s .failCycleLocked (owner .flag , path )
178+ s .mu .Unlock ()
179+ return false
180+ }
181+ if s .waiting [owner .flag ] == nil {
182+ s .waiting [owner .flag ] = make (map [FeatureFlag ]int )
183+ }
184+ s.waiting [owner.flag ][feature ]++
185+ }
164186 s .mu .Unlock ()
165187
188+ if owner != nil {
189+ defer s .clearWait (owner .flag , feature )
190+ }
166191 if found {
167192 select {
168193 case <- result .ready :
@@ -175,25 +200,77 @@ func (s *featureState) enabled(ctx context.Context, feature FeatureFlag) bool {
175200 completed := false
176201 defer func () {
177202 if ! completed {
203+ s .mu .Lock ()
204+ result .failed = true
205+ result .done = true
178206 close (result .ready )
207+ s .mu .Unlock ()
179208 }
180209 }()
181210
182- resolutionCtx := context .WithValue (ctx , resolvingFeatureContextKey {}, & resolvingFeature {
183- flag : feature ,
184- parent : resolvingFeatureFromContext (ctx ),
185- })
211+ resolutionCtx := context .WithValue (ctx , resolvingFeatureContextKey {}, & resolvingFeature {flag : feature })
186212 enabled , err := s .checker (resolutionCtx , feature )
187213 if err != nil {
188214 fmt .Fprintf (os .Stderr , "Feature flag check error for %q: %v\n " , feature , err )
189215 enabled = false
190216 }
217+ s .mu .Lock ()
218+ if result .failed {
219+ enabled = false
220+ }
191221 result .enabled = enabled
222+ result .done = true
192223 completed = true
193224 close (result .ready )
225+ s .mu .Unlock ()
194226 return enabled
195227}
196228
229+ func (s * featureState ) pathLocked (current , target FeatureFlag , seen map [FeatureFlag ]bool ) []FeatureFlag {
230+ if current == target {
231+ return []FeatureFlag {current }
232+ }
233+ if seen == nil {
234+ seen = make (map [FeatureFlag ]bool )
235+ }
236+ if seen [current ] {
237+ return nil
238+ }
239+ seen [current ] = true
240+ for next := range s .waiting [current ] {
241+ if path := s .pathLocked (next , target , seen ); path != nil {
242+ return append ([]FeatureFlag {current }, path ... )
243+ }
244+ }
245+ return nil
246+ }
247+
248+ func (s * featureState ) failCycleLocked (owner FeatureFlag , path []FeatureFlag ) {
249+ if result := s .results [owner ]; result != nil {
250+ result .failed = true
251+ }
252+ for _ , feature := range path {
253+ if result := s .results [feature ]; result != nil {
254+ result .failed = true
255+ }
256+ }
257+ fmt .Fprintf (os .Stderr , "Feature flag resolution cycle detected for %q\n " , path [0 ])
258+ }
259+
260+ func (s * featureState ) clearWait (owner , target FeatureFlag ) {
261+ s .mu .Lock ()
262+ if targets := s .waiting [owner ]; targets != nil {
263+ targets [target ]--
264+ if targets [target ] == 0 {
265+ delete (targets , target )
266+ }
267+ }
268+ if len (s .waiting [owner ]) == 0 {
269+ delete (s .waiting , owner )
270+ }
271+ s .mu .Unlock ()
272+ }
273+
197274func resolvingFeatureFromContext (ctx context.Context ) * resolvingFeature {
198275 feature , _ := ctx .Value (resolvingFeatureContextKey {}).(* resolvingFeature )
199276 return feature
0 commit comments