Skip to content

Commit a45070e

Browse files
committed
docs: add docs about sticky resolve
1 parent 9bc38b5 commit a45070e

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

openfeature-provider-local/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ You need two types of credentials:
9090

9191
Both can be obtained from your Confidence dashboard.
9292

93+
## Sticky Resolve
94+
95+
The provider supports **Sticky Resolve** for consistent variant assignments across flag evaluations. This ensures users receive the same variant even when their targeting attributes change, and enables pausing experiment intake.
96+
97+
**By default, sticky assignments are managed by Confidence servers.** When sticky assignment data is needed, the provider makes a network call to Confidence, which maintains the sticky repository server-side with automatic 90-day TTL management. This is a fully supported production approach that requires no additional setup.
98+
99+
100+
Optionally, you can implement a custom `MaterializationRepository` to manage sticky assignments in your own storage (Redis, database, etc.) to eliminate network calls and improve latency:
101+
102+
```java
103+
// Optional: Custom storage for sticky assignments
104+
MaterializationRepository repository = new RedisMaterializationRepository(jedisPool, "myapp");
105+
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
106+
apiSecret,
107+
clientSecret,
108+
repository
109+
);
110+
```
111+
112+
For detailed information on how sticky resolve works and how to implement custom storage backends, see [STICKY_RESOLVE.md](STICKY_RESOLVE.md).
113+
93114
## Requirements
94115

95116
- Java 17+
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Sticky Resolve Documentation
2+
3+
## Overview
4+
5+
Sticky Resolve ensures users receive the same variant throughout an experiment, even if their targeting attributes change or you pause new assignments.
6+
7+
**Two main use cases:**
8+
1. **Consistent experience** - User moves countries but keeps the same variant
9+
2. **Pause intake** - Stop new assignments while maintaining existing ones
10+
11+
**Default behavior:** Sticky assignments are managed by Confidence servers with automatic 90-day TTL. When needed, the provider makes a network call to Confidence. No setup required.
12+
13+
## How It Works
14+
15+
### Default: Server-Side Storage (RemoteResolverFallback)
16+
17+
**Flow:**
18+
1. Local WASM resolver attempts to resolve
19+
2. If sticky data needed → network call to Confidence
20+
3. Confidence checks its sticky repository, returns variant
21+
4. Assignment stored server-side with 90-day TTL (auto-renewed on access)
22+
23+
**Server-side configuration (in Confidence UI):**
24+
- Optionally skip targeting criteria for sticky assignments
25+
- Pause/resume new entity intake
26+
- Automatic TTL management
27+
28+
### Custom: Local Storage (MaterializationRepository)
29+
30+
Implement `MaterializationRepository` to store assignments locally and eliminate network calls.
31+
32+
**Interface:**
33+
```java
34+
public interface MaterializationRepository extends StickyResolveStrategy {
35+
// Load assignments for a unit (e.g., user ID)
36+
CompletableFuture<Map<String, MaterializationInfo>> loadMaterializedAssignmentsForUnit(
37+
String unit, String materialization);
38+
39+
// Store new assignments
40+
CompletableFuture<Void> storeAssignment(
41+
String unit, Map<String, MaterializationInfo> assignments);
42+
}
43+
```
44+
45+
**MaterializationInfo structure:**
46+
```java
47+
record MaterializationInfo(
48+
boolean isUnitInMaterialization,
49+
Map<String, String> ruleToVariant // rule ID -> variant name
50+
)
51+
```
52+
53+
## Implementation Examples
54+
55+
### In-Memory (Testing/Development)
56+
57+
```java
58+
public class InMemoryMaterializationRepository implements MaterializationRepository {
59+
private final Map<String, Map<String, MaterializationInfo>> storage = new ConcurrentHashMap<>();
60+
61+
@Override
62+
public CompletableFuture<Map<String, MaterializationInfo>> loadMaterializedAssignmentsForUnit(
63+
String unit, String materialization) {
64+
return CompletableFuture.supplyAsync(() -> {
65+
Map<String, MaterializationInfo> unitAssignments = storage.get(unit);
66+
if (unitAssignments == null || !unitAssignments.containsKey(materialization)) {
67+
return Map.of();
68+
}
69+
return Map.of(materialization, unitAssignments.get(materialization));
70+
});
71+
}
72+
73+
@Override
74+
public CompletableFuture<Void> storeAssignment(
75+
String unit, Map<String, MaterializationInfo> assignments) {
76+
return CompletableFuture.runAsync(() -> {
77+
storage.compute(unit, (key, existing) -> {
78+
if (existing == null) {
79+
return new ConcurrentHashMap<>(assignments);
80+
}
81+
existing.putAll(assignments);
82+
return existing;
83+
});
84+
});
85+
}
86+
87+
@Override
88+
public void close() {
89+
storage.clear();
90+
}
91+
}
92+
```
93+
94+
### Redis (Production)
95+
96+
```java
97+
public class RedisMaterializationRepository implements MaterializationRepository {
98+
private final JedisPool jedisPool;
99+
private final ObjectMapper objectMapper = new ObjectMapper();
100+
private static final int TTL_SECONDS = 90 * 24 * 60 * 60; // 90 days
101+
102+
public RedisMaterializationRepository(JedisPool jedisPool) {
103+
this.jedisPool = jedisPool;
104+
}
105+
106+
@Override
107+
public CompletableFuture<Map<String, MaterializationInfo>> loadMaterializedAssignmentsForUnit(
108+
String unit, String materialization) {
109+
return CompletableFuture.supplyAsync(() -> {
110+
try (var jedis = jedisPool.getResource()) {
111+
String key = "sticky:" + unit + ":" + materialization;
112+
String value = jedis.get(key);
113+
114+
if (value == null) return Map.of();
115+
116+
// Renew TTL on read (matching Confidence behavior)
117+
jedis.expire(key, TTL_SECONDS);
118+
119+
MaterializationInfo info = objectMapper.readValue(value, MaterializationInfo.class);
120+
return Map.of(materialization, info);
121+
} catch (Exception e) {
122+
throw new RuntimeException("Failed to load from Redis", e);
123+
}
124+
});
125+
}
126+
127+
@Override
128+
public CompletableFuture<Void> storeAssignment(
129+
String unit, Map<String, MaterializationInfo> assignments) {
130+
return CompletableFuture.runAsync(() -> {
131+
try (var jedis = jedisPool.getResource()) {
132+
for (var entry : assignments.entrySet()) {
133+
String key = "sticky:" + unit + ":" + entry.getKey();
134+
String value = objectMapper.writeValueAsString(entry.getValue());
135+
jedis.setex(key, TTL_SECONDS, value);
136+
}
137+
} catch (Exception e) {
138+
// Don't fail resolve on storage errors
139+
System.err.println("Failed to store to Redis: " + e.getMessage());
140+
}
141+
});
142+
}
143+
144+
@Override
145+
public void close() {
146+
jedisPool.close();
147+
}
148+
}
149+
```
150+
151+
### Usage
152+
153+
```java
154+
MaterializationRepository repository = new RedisMaterializationRepository(jedisPool);
155+
156+
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
157+
apiSecret,
158+
clientSecret,
159+
repository
160+
);
161+
```
162+
163+
## Best Practices
164+
165+
1. **Fail gracefully** - Storage errors shouldn't fail flag resolution
166+
2. **Use 90-day TTL** - Match Confidence's default behavior, renew on read
167+
3. **Connection pooling** - Use pools for Redis/DB connections
168+
4. **Monitor metrics** - Track cache hit rate, storage latency, errors
169+
5. **Test both paths** - Missing assignments (cold start) and existing assignments
170+
171+
## When to Use Custom Storage
172+
173+
| Strategy | Best For | Trade-offs |
174+
|----------|----------|------------|
175+
| **RemoteResolverFallback** (default) | Most apps | Simple, managed by Confidence. Network calls when needed. |
176+
| **MaterializationRepository** (in-memory) | Single-instance apps, testing | Fast, no network. Lost on restart. |
177+
| **MaterializationRepository** (Redis/DB) | High-traffic apps, offline scenarios | No network calls. Requires storage infra. |
178+
179+
**Start with the default.** Only implement custom storage if you need to eliminate network calls or work offline.
180+
181+
## Additional Resources
182+
183+
- [Confidence Sticky Assignments Documentation](https://confidence.spotify.com/docs/flags/audience#sticky-assignments)

0 commit comments

Comments
 (0)