62 lines
feature/service.go
Feature flag storage and per-user evaluation using a deterministic bucket hash.
// Package feature manages feature flags and percentage-based user rollouts.
package feature
 
import (
	"hash/fnv"
	"sync"
)
 
// Flag configures a named feature toggle.
// Percentage controls what fraction of users see the feature: 0 disables it for all,
// 100 enables it for all, and values in between use a deterministic per-user bucket.
type Flag struct {
	Name       string
	Percentage int  // 0–100
	Enabled    bool // true overrides Percentage and enables for all users
}
 
// Service stores and evaluates feature flags.
type Service struct {
	mu    sync.RWMutex
	flags map[string]Flag
}
 
// NewService returns a Service with no configured flags.
func NewService() *Service {
	return &Service{flags: make(map[string]Flag)}
}
 
// Set adds or replaces a flag configuration.
func (s *Service) Set(name string, f Flag) {
	s.mu.Lock()
	defer s.mu.Unlock()
	f.Name = name
	s.flags[name] = f
}
 
// Evaluate reports whether flagName is active for userID.
// Returns false if the flag is not configured.
// If Enabled is true, returns true for all users.
// Otherwise, a deterministic bucket is computed as fnv64(userID + ":" + flagName) mod 100;
// the flag is active when bucket < Percentage.
func (s *Service) Evaluate(flagName, userID string) bool {
	s.mu.RLock()
	f, ok := s.flags[flagName]
	s.mu.RUnlock()
	if !ok {
		return false
	}
	if f.Enabled {
		return true
	}
	if f.Percentage <= 0 {
		return false
	}
	if f.Percentage >= 100 {
		return true
	}
	h := fnv.New64a()
	h.Write([]byte(userID))
	bucket := int(h.Sum64() % 100)
	return bucket < f.Percentage
}