Skip to content

Commit f5b0e91

Browse files
committed
Implement config overlay pattern and refactor feature flags for extensibility
Config Overlay System: - Add internal/config/overlay.go with generic ConfigValue[T] type - Support client > server > default priority across all driver configs - Refactor telemetry to use ConfigValue[bool] instead of *bool - Add comprehensive tests for overlay pattern (20+ test cases) Feature Flag Refactoring: - Support multiple feature flags in single HTTP request - Change from single flag to map[string]bool storage - Add getAllFeatureFlags() for easy flag registration - Add getFeatureFlag() generic method for any flag - Keep isTelemetryEnabled() API backward compatible - Add ADDING_FEATURE_FLAGS.md guide for extending system Documentation Updates: - Document config overlay pattern in DESIGN.md - Clarify synchronous fetch behavior (10s timeout, RetryableClient) - Document blocking scenarios and thundering herd protection - Add rationale for synchronous vs async approach Benefits: - Easy to add new flags (3 lines of code) - Single HTTP request fetches all flags (efficient) - Reusable ConfigValue[T] pattern for all driver configs - All 89 telemetry tests passing
1 parent e12933d commit f5b0e91

8 files changed

Lines changed: 763 additions & 132 deletions

File tree

internal/config/overlay.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strconv"
7+
)
8+
9+
// ConfigValue represents a configuration value that can be set by client or resolved from server.
10+
// This implements the config overlay pattern: client > server > default
11+
//
12+
// T is the type of the configuration value (bool, string, int, etc.)
13+
//
14+
// Example usage:
15+
//
16+
// type MyConfig struct {
17+
// EnableFeature ConfigValue[bool]
18+
// BatchSize ConfigValue[int]
19+
// }
20+
//
21+
// // Client explicitly sets value (overrides server)
22+
// config.EnableFeature = NewConfigValue(true)
23+
//
24+
// // Client doesn't set value (use server)
25+
// config.EnableFeature = ConfigValue[bool]{} // nil/unset
26+
//
27+
// // Resolve value with overlay priority
28+
// enabled := config.EnableFeature.Resolve(ctx, serverResolver, defaultValue)
29+
type ConfigValue[T any] struct {
30+
// value is the client-set configuration value
31+
// nil = not set by client (use server config)
32+
// non-nil = explicitly set by client (overrides server)
33+
value *T
34+
}
35+
36+
// NewConfigValue creates a ConfigValue with a client-set value.
37+
// The value will override any server-side configuration.
38+
func NewConfigValue[T any](value T) ConfigValue[T] {
39+
return ConfigValue[T]{value: &value}
40+
}
41+
42+
// IsSet returns true if the client explicitly set this configuration value.
43+
func (cv ConfigValue[T]) IsSet() bool {
44+
return cv.value != nil
45+
}
46+
47+
// Get returns the client-set value and whether it was set.
48+
// If not set, returns zero value and false.
49+
func (cv ConfigValue[T]) Get() (T, bool) {
50+
if cv.value != nil {
51+
return *cv.value, true
52+
}
53+
var zero T
54+
return zero, false
55+
}
56+
57+
// ServerResolver defines how to fetch a configuration value from the server.
58+
// Implementations should handle caching, retries, and error handling.
59+
type ServerResolver[T any] interface {
60+
// Resolve fetches the configuration value from the server.
61+
// Returns the value and any error encountered.
62+
// On error, the config overlay will fall back to the default value.
63+
Resolve(ctx context.Context, host string, httpClient *http.Client) (T, error)
64+
}
65+
66+
// Resolve applies config overlay priority to determine the final value:
67+
//
68+
// Priority 1: Client Config - if explicitly set (overrides server)
69+
// Priority 2: Server Config - resolved via serverResolver (when client doesn't set)
70+
// Priority 3: Default Value - used when server unavailable/errors (fail-safe)
71+
//
72+
// Parameters:
73+
// - ctx: Context for server requests
74+
// - serverResolver: How to fetch from server (can be nil if no server config)
75+
// - defaultValue: Fail-safe default when client unset and server unavailable
76+
//
77+
// Returns: The resolved configuration value following overlay priority
78+
func (cv ConfigValue[T]) Resolve(
79+
ctx context.Context,
80+
serverResolver ServerResolver[T],
81+
defaultValue T,
82+
) T {
83+
// Priority 1: Client explicitly set (overrides everything)
84+
if cv.value != nil {
85+
return *cv.value
86+
}
87+
88+
// Priority 2: Try server config (if resolver provided)
89+
if serverResolver != nil {
90+
// Note: We pass empty host/httpClient here. Actual resolver should have these injected
91+
// This is a simplified interface - real usage would inject dependencies
92+
if serverValue, err := serverResolver.Resolve(ctx, "", nil); err == nil {
93+
return serverValue
94+
}
95+
}
96+
97+
// Priority 3: Fail-safe default
98+
return defaultValue
99+
}
100+
101+
// ResolveWithContext is a more flexible version that takes host and httpClient.
102+
// This is the recommended method for production use.
103+
func (cv ConfigValue[T]) ResolveWithContext(
104+
ctx context.Context,
105+
host string,
106+
httpClient *http.Client,
107+
serverResolver ServerResolver[T],
108+
defaultValue T,
109+
) T {
110+
// Priority 1: Client explicitly set (overrides everything)
111+
if cv.value != nil {
112+
return *cv.value
113+
}
114+
115+
// Priority 2: Try server config (if resolver provided)
116+
if serverResolver != nil {
117+
if serverValue, err := serverResolver.Resolve(ctx, host, httpClient); err == nil {
118+
return serverValue
119+
}
120+
}
121+
122+
// Priority 3: Fail-safe default
123+
return defaultValue
124+
}
125+
126+
// ParseBoolConfigValue parses a string value into a ConfigValue[bool].
127+
// Returns unset ConfigValue if the parameter is not present.
128+
//
129+
// Example:
130+
//
131+
// params := map[string]string{"enableFeature": "true"}
132+
// value := ParseBoolConfigValue(params, "enableFeature")
133+
// // value.IsSet() == true, value.Get() == (true, true)
134+
func ParseBoolConfigValue(params map[string]string, key string) ConfigValue[bool] {
135+
if v, ok := params[key]; ok {
136+
enabled := (v == "true" || v == "1")
137+
return NewConfigValue(enabled)
138+
}
139+
return ConfigValue[bool]{} // Unset
140+
}
141+
142+
// ParseStringConfigValue parses a string value into a ConfigValue[string].
143+
// Returns unset ConfigValue if the parameter is not present.
144+
func ParseStringConfigValue(params map[string]string, key string) ConfigValue[string] {
145+
if v, ok := params[key]; ok {
146+
return NewConfigValue(v)
147+
}
148+
return ConfigValue[string]{} // Unset
149+
}
150+
151+
// ParseIntConfigValue parses a string value into a ConfigValue[int].
152+
// Returns unset ConfigValue if the parameter is not present or invalid.
153+
func ParseIntConfigValue(params map[string]string, key string) ConfigValue[int] {
154+
if v, ok := params[key]; ok {
155+
if i, err := strconv.Atoi(v); err == nil {
156+
return NewConfigValue(i)
157+
}
158+
}
159+
return ConfigValue[int]{} // Unset
160+
}

0 commit comments

Comments
 (0)