Skip to content

Commit e0fdefd

Browse files
committed
test: add example InMemoryMaterializationRepo
1 parent a45070e commit e0fdefd

File tree

2 files changed

+93
-96
lines changed

2 files changed

+93
-96
lines changed

openfeature-provider-local/STICKY_RESOLVE.md

Lines changed: 4 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -54,104 +54,12 @@ record MaterializationInfo(
5454

5555
### In-Memory (Testing/Development)
5656

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-
```
57+
[Here is an example](src/test/java/com/spotify/confidence/InMemoryMaterializationRepoExample.java) on how to implement a simple in-memory `MaterializationRepository`. The same approach can be used with other more persistent storages (like Redis or similar) which is highly recommended for production use cases.
15058

151-
### Usage
59+
#### Usage
15260

15361
```java
154-
MaterializationRepository repository = new RedisMaterializationRepository(jedisPool);
62+
MaterializationRepository repository = new InMemoryMaterializationRepoExample();
15563

15664
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
15765
apiSecret,
@@ -174,7 +82,7 @@ OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
17482
|----------|----------|------------|
17583
| **RemoteResolverFallback** (default) | Most apps | Simple, managed by Confidence. Network calls when needed. |
17684
| **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. |
85+
| **MaterializationRepository** (Redis/DB) | Distributed/Multi instance apps | No network calls. Requires storage infra. |
17886

17987
**Start with the default.** Only implement custom storage if you need to eliminate network calls or work offline.
18088

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.example;
2+
3+
import com.spotify.confidence.MaterializationInfo;
4+
import com.spotify.confidence.MaterializationRepository;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
public class InMemoryMaterializationRepoExample implements MaterializationRepository {
13+
14+
private static final Logger logger = LoggerFactory.getLogger(InMemoryMaterializationRepo.class);
15+
private final Map<String, Map<String, MaterializationInfo>> storage = new ConcurrentHashMap<>();
16+
17+
/**
18+
* Helper method to create a map with a default, empty MaterializationInfo.
19+
*
20+
* @param key The key to use in the returned map.
21+
* @return A map containing the key and a default MaterializationInfo object.
22+
*/
23+
private static Map<String, MaterializationInfo> createEmptyMap(String key) {
24+
MaterializationInfo emptyInfo = new MaterializationInfo(false, new HashMap<>());
25+
Map<String, MaterializationInfo> map = new HashMap<>();
26+
map.put(key, emptyInfo);
27+
return map;
28+
}
29+
30+
@Override
31+
public CompletableFuture<Map<String, MaterializationInfo>> loadMaterializedAssignmentsForUnit(
32+
String unit, String materialization) {
33+
Map<String, MaterializationInfo> unitAssignments = storage.get(unit);
34+
if (unitAssignments != null) {
35+
if (unitAssignments.containsKey(materialization)) {
36+
Map<String, MaterializationInfo> result = new HashMap<>();
37+
result.put(materialization, unitAssignments.get(materialization));
38+
logger.debug("Cache hit for unit: {}, materialization: {}", unit, materialization);
39+
return CompletableFuture.supplyAsync(() -> result);
40+
} else {
41+
logger.debug(
42+
"Materialization {} not found in cached data for unit: {}", materialization, unit);
43+
return CompletableFuture.completedFuture(createEmptyMap(materialization));
44+
}
45+
}
46+
47+
// If unitAssignments was null (cache miss for the unit), return an empty map structure.
48+
return CompletableFuture.completedFuture(createEmptyMap(materialization));
49+
}
50+
51+
@Override
52+
public CompletableFuture<Void> storeAssignment(
53+
String unit, Map<String, MaterializationInfo> assignments) {
54+
if (unit == null) {
55+
return CompletableFuture.completedFuture(null);
56+
}
57+
58+
// Use 'compute' for an atomic update operation on the ConcurrentHashMap.
59+
storage.compute(
60+
unit,
61+
(k, existingEntry) -> {
62+
if (existingEntry == null) {
63+
// If no entry exists, create a new one.
64+
// We create a new HashMap to avoid storing a reference to the potentially mutable
65+
// 'assignments' map.
66+
return assignments == null ? new HashMap<>() : new HashMap<>(assignments);
67+
} else {
68+
// If an entry exists, merge the new assignments into it.
69+
// This is equivalent to Kotlin's 'existingEntry.plus(assignments ?: emptyMap())'.
70+
Map<String, MaterializationInfo> newEntry = new HashMap<>(existingEntry);
71+
if (assignments != null) {
72+
newEntry.putAll(assignments);
73+
}
74+
return newEntry;
75+
}
76+
});
77+
78+
int assignmentCount = (assignments != null) ? assignments.size() : 0;
79+
logger.debug("Stored {} assignments for unit: {}", assignmentCount, unit);
80+
81+
return CompletableFuture.completedFuture(null);
82+
}
83+
84+
@Override
85+
public void close() {
86+
storage.clear();
87+
logger.debug("In-memory storage cleared.");
88+
}
89+
}

0 commit comments

Comments
 (0)