Skip to content

Commit 9b0d30f

Browse files
authored
[FFM-10816] - Remove metrics flush on map overflow (#183)
* [FFM-10816] - Remove metrics flush on map overflow What When too many evaluations are processed and with enough targets the internal metrics map we use will hit its limit very quickly. This causes the metrics to be flushed to the server. Why Flushing metrics is unbound and can result in more pressure on the server than necessary Testing Manual + unit
1 parent 7fd468d commit 9b0d30f

File tree

5 files changed

+71
-30
lines changed

5 files changed

+71
-30
lines changed

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dependencyResolutionManagement {
44
versionCatalogs {
55
libs {
66
// main sdk version
7-
version('sdk', '1.5.1');
7+
version('sdk', '1.5.2');
88

99
// sdk deps
1010
version('okhttp3', '4.12.0')

src/main/java/io/harness/cf/client/api/BaseConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class BaseConfig {
2727
@Getter(AccessLevel.NONE)
2828
private final int frequency = 60; // unit: second
2929

30-
@Builder.Default private final int bufferSize = 2048;
30+
@Builder.Default private final int bufferSize = 5000;
3131

3232
// Flag to set all attributes as private
3333
@Deprecated @Builder.Default private final boolean allAttributesPrivate = false;

src/main/java/io/harness/cf/client/api/MetricsProcessor.java

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.harness.cf.client.api;
22

3+
import static io.harness.cf.client.common.SdkCodes.warnMetricsBufferFull;
34
import static io.harness.cf.client.common.Utils.shutdownExecutorService;
45
import static java.util.concurrent.TimeUnit.SECONDS;
56

@@ -70,6 +71,10 @@ private void transferValueIntoMapAtomicallyAndUpdateTo(
7071
return newValue;
7172
});
7273
}
74+
75+
public boolean containsKey(K key) {
76+
return freqMap.containsKey(key);
77+
}
7378
}
7479

7580
private static final String FEATURE_NAME_ATTRIBUTE = "featureName";
@@ -91,55 +96,66 @@ private void transferValueIntoMapAtomicallyAndUpdateTo(
9196
private static final Target globalTarget =
9297
Target.builder().name(GLOBAL_TARGET_NAME).identifier(GLOBAL_TARGET).build();
9398

99+
private static final int MAX_SENT_TARGETS_TO_RETAIN = 100_000;
100+
private static final int MAX_FREQ_MAP_TO_RETAIN = 10_000;
101+
102+
private static final LongAdder evalCounter = new LongAdder();
103+
private static final LongAdder metricsEvalsDropped = new LongAdder();
104+
private static final LongAdder targetsSeenDropped = new LongAdder();
94105
private final Connector connector;
95106
private final BaseConfig config;
96107
private final FrequencyMap<MetricEvent> frequencyMap;
97-
private final Set<Target> uniqueTargetSet;
108+
private final Set<Target> targetsSeen;
98109

99110
private ScheduledFuture<?> runningTask = null;
100111
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
101112

102113
private final LongAdder metricsSent = new LongAdder();
114+
private final int maxFreqMapSize;
103115

104116
public MetricsProcessor(
105117
@NonNull Connector connector, @NonNull BaseConfig config, @NonNull MetricsCallback callback) {
106118
this.connector = connector;
107119
this.config = config;
108120
this.frequencyMap = new FrequencyMap<>();
109-
this.uniqueTargetSet = ConcurrentHashMap.newKeySet();
121+
this.targetsSeen = ConcurrentHashMap.newKeySet();
122+
this.maxFreqMapSize = clamp(config.getBufferSize(), 2048, MAX_FREQ_MAP_TO_RETAIN);
110123
callback.onMetricsReady();
111124
}
112125

126+
private int clamp(int value, int lower, int higher) {
127+
return Math.max(lower, Math.min(higher, value));
128+
}
129+
113130
@Deprecated /* The name of this method no longer makes sense since we moved to a map, kept for source compatibility */
114131
public void pushToQueue(Target target, String featureName, Variation variation) {
115132
registerEvaluation(target, featureName, variation);
116133
}
117134

118135
void registerEvaluation(Target target, String featureName, Variation variation) {
119136

120-
final int freqMapSize = frequencyMap.size();
137+
Target metricTarget = globalTarget;
121138

122-
if (freqMapSize > config.getBufferSize()) {
123-
if (log.isWarnEnabled()) {
124-
log.warn(
125-
"Metric frequency map exceeded buffer size ({} > {}), force flushing",
126-
freqMapSize,
127-
config.getBufferSize());
139+
if (target != null) {
140+
if (!targetsSeen.contains(target) && targetsSeen.size() + 1 > MAX_SENT_TARGETS_TO_RETAIN) {
141+
targetsSeenDropped.increment();
142+
} else {
143+
targetsSeen.add(target);
144+
if (!config.isGlobalTargetEnabled()) {
145+
metricTarget = target;
146+
}
128147
}
129-
// If the map is starting to grow too much then push the events now and reset the counters
130-
scheduler.schedule(this::runOneIteration, 0, SECONDS);
131148
}
132149

133-
Target metricTarget = globalTarget;
150+
final MetricEvent metricsEvent = new MetricEvent(featureName, metricTarget, variation);
134151

135-
if (target != null) {
136-
uniqueTargetSet.add(target);
137-
if (!config.isGlobalTargetEnabled()) {
138-
metricTarget = target;
139-
}
152+
if (!frequencyMap.containsKey(metricsEvent) && frequencyMap.size() + 1 > maxFreqMapSize) {
153+
metricsEvalsDropped.increment();
154+
} else {
155+
frequencyMap.increment(metricsEvent);
140156
}
141157

142-
frequencyMap.increment(new MetricEvent(featureName, metricTarget, variation));
158+
evalCounter.increment();
143159
}
144160

145161
/** This method sends the metrics data to the analytics server and resets the cache */
@@ -259,15 +275,23 @@ private void addTargetData(Metrics metrics, Target target) {
259275

260276
void runOneIteration() {
261277
Thread.currentThread().setName("MetricsThread");
278+
279+
final long droppedEvals = metricsEvalsDropped.sumThenReset();
280+
final long droppedTargets = targetsSeenDropped.sumThenReset();
281+
282+
if (droppedEvals > 0 || droppedTargets > 0) {
283+
warnMetricsBufferFull(droppedEvals, droppedTargets);
284+
}
285+
262286
if (log.isDebugEnabled()) {
263287
log.debug(
264288
"Drain metrics queue : frequencyMap size={} uniqueTargetSet size={}",
265289
frequencyMap.size(),
266-
uniqueTargetSet.size());
290+
targetsSeen.size());
267291
}
268-
sendDataAndResetCache(frequencyMap.drainToMap(), new HashSet<>(uniqueTargetSet));
292+
sendDataAndResetCache(frequencyMap.drainToMap(), new HashSet<>(targetsSeen));
269293

270-
uniqueTargetSet.clear();
294+
targetsSeen.clear();
271295
}
272296

273297
public void start() {
@@ -328,7 +352,15 @@ long getQueueSize() {
328352
}
329353

330354
long getTargetSetSize() {
331-
return uniqueTargetSet.size();
355+
return targetsSeen.size();
356+
}
357+
358+
long getMetricsEvalsDropped() {
359+
return metricsEvalsDropped.sum();
360+
}
361+
362+
long getTargetsSeenDropped() {
363+
return targetsSeenDropped.sum();
332364
}
333365

334366
void reset() {

src/main/java/io/harness/cf/client/common/SdkCodes.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public static void warnPostMetricsFailed(String reason) {
6565
log.warn(sdkErrMsg(7002, Optional.ofNullable(reason)));
6666
}
6767

68+
public static void warnMetricsBufferFull(long droppedEvals, long droppedTargets) {
69+
log.warn(
70+
sdkErrMsg(
71+
7008,
72+
Optional.of(
73+
"- evals dropped: " + droppedEvals + " targets dropped: " + droppedTargets)));
74+
}
75+
6876
public static void warnDefaultVariationServed(String identifier, Target target, String def) {
6977
String targetId = (target == null) ? "null" : target.getIdentifier();
7078
String msg = String.format("identifier=%s, target=%s, default=%s", identifier, targetId, def);
@@ -111,7 +119,8 @@ public static void warnBucketByAttributeNotFound(String bucketBy, String usingVa
111119
// SDK_METRICS_7xxx
112120
{"7000", "Metrics thread started, intervalMs:"},
113121
{"7001", "Metrics thread exited"},
114-
{"7002", "Posting metrics failed, reason:"}
122+
{"7002", "Posting metrics failed, reason:"},
123+
{"7008", "Metrics buffer is full and metrics will be discarded"}
115124
})
116125
.collect(Collectors.toMap(entry -> Integer.parseInt(entry[0]), entry -> entry[1]));
117126

src/test/java/io/harness/cf/client/api/MetricsProcessorTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ public void setup() {
3434
MockitoAnnotations.openMocks(this);
3535
metricsProcessor =
3636
Mockito.spy(
37-
new MetricsProcessor(
38-
connector, BaseConfig.builder().bufferSize(BUFFER_SIZE).build(), this));
37+
new MetricsProcessor(connector, BaseConfig.builder().bufferSize(10_001).build(), this));
3938

4039
metricsProcessor.reset();
4140
}
@@ -79,7 +78,7 @@ public void shouldNotThrowOutOfMemoryErrorWhenCreatingThreads() throws Interrupt
7978
Variation variation =
8079
Variation.builder().identifier("true" + v).name("name" + v).value("true").build();
8180

82-
metricsProcessor.pushToQueue(target, feature.getFeature(), variation);
81+
metricsProcessor.registerEvaluation(target, feature.getFeature(), variation);
8382

8483
maxQueueMapSize = Math.max(maxQueueMapSize, metricsProcessor.getQueueSize());
8584
maxUniqueTargetSetSize =
@@ -89,13 +88,14 @@ public void shouldNotThrowOutOfMemoryErrorWhenCreatingThreads() throws Interrupt
8988

9089
if (t % 10 == 0) {
9190
log.info(
92-
"Metrics frequency map (cur: {} max: {}) Unique targets (cur: {} max: {}) Events sent ({}) Events pending ({})",
91+
"Metrics frequency map (cur: {} max: {}) Unique targets (cur: {} max: {}) Events sent ({}) Events pending ({}) Dropped ({})",
9392
metricsProcessor.getQueueSize(),
9493
maxQueueMapSize,
9594
metricsProcessor.getTargetSetSize(),
9695
maxUniqueTargetSetSize,
9796
metricsProcessor.getMetricsSent(),
98-
metricsProcessor.getPendingMetricsToBeSent());
97+
metricsProcessor.getPendingMetricsToBeSent(),
98+
metricsProcessor.getMetricsEvalsDropped());
9999

100100
metricsProcessor.flushQueue(); // mimic scheduled job
101101
}

0 commit comments

Comments
 (0)