Skip to content

Commit

Permalink
support dailyLimit
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Jun 20, 2024
1 parent 4a8ee69 commit e9fbdd0
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -72,11 +70,19 @@ public Count get(Event event) {
}

public void put(Event event) {
Count count = newOrUpdateExisting(event);
Count count = newOrUpdateCount(event);
put(event, count);
}

private Count newOrUpdateExisting(Event event) {
EventCounts put(Event event, Count count) {
if (event == null) {
return null;
}
counts.put(event.getName(), toString(count));
return this;
}

private Count newOrUpdateCount(Event event) {
Count count = get(event);
if (count != null) {
// update existing
Expand Down Expand Up @@ -115,14 +121,6 @@ private int toTotal(String value) {
}
}

EventCounts put(Event event, Count count) {
if (event == null) {
return null;
}
counts.put(event.getName(), toString(count));
return this;
}

String toString(@NotNull Count count) {
long epochSecond = count.lastOccurrence.toEpochSecond(ZonedDateTime.now().getOffset());
return epochSecond + COUNT_VALUES_SEPARATOR + count.total;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
import java.io.IOException;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;

import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.EventCounts.*;

public class EventLimits implements IEventLimits {

static final Duration DEFAULT_REFRESH_PERIOD = Duration.ofHours(6);
Expand All @@ -40,7 +42,11 @@ public EventLimits(String pluginId) {
this(pluginId, null, PluginLimitsDeserialization::create, new Configurations(), new EventCounts());
}

EventLimits(String pluginId, List<PluginLimits> limits, PluginLimitsFactory factory, Configurations configuration, EventCounts counts) {
EventLimits(String pluginId,
List<PluginLimits> limits,
PluginLimitsFactory factory,
Configurations configuration,
EventCounts counts) {
this.pluginId = pluginId;
this.limits = limits;
this.factory = factory;
Expand All @@ -50,15 +56,36 @@ public EventLimits(String pluginId) {

public boolean canSend(Event event) {
List<PluginLimits> all = getAllLimits();
return canSend(event, getPluginLimits(pluginId, all))
&& canSend(event, getDefaultLimits(all));
return canSend(event, counts, getPluginLimits(pluginId, all))
&& canSend(event, counts, getDefaultLimits(all));
}

public void wasSent(Event event) {
counts.put(event);
}

private boolean canSend(Event event, EventCounts counts, PluginLimits limits) {
if (limits == null) {
return true;
}
return limits.canSend(event, getApplicableTotal(counts.get(event)));
}

private int getApplicableTotal(Count count) {
if (occurredToday(count)) {
return count.getTotal();
} else {
return 0;
}
}

private boolean canSend(Event event, PluginLimits limits) {
return limits == null
|| limits.canSend(event);
private static boolean occurredToday(Count count) {
return count != null
&& count.getLastOccurrence() != null
&& LocalDate.now().atStartOfDay().isBefore(count.getLastOccurrence());
}


/* for testing purposes */
List<PluginLimits> getAllLimits() {
PluginLimits defaults = getDefaultLimits(limits);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public interface Filter {

boolean isExcludedByRatio(float percentile);

boolean isWithinDailyLimit(int total);

class EventPropertyFilter implements Filter {
private final String name;
private final BasicGlobPattern glob;
Expand All @@ -46,14 +48,18 @@ public boolean isExcludedByRatio(float percentile) {
return false;
}

@Override
public boolean isWithinDailyLimit(int total) {
return true;
}
}

class EventNameFilter implements Filter {
private final BasicGlobPattern name;
private final float ratio;
private final String dailyLimit;
private final int dailyLimit;

EventNameFilter(String name, float ratio, String dailyLimit) {
EventNameFilter(String name, float ratio, int dailyLimit) {
this.name = BasicGlobPattern.compile(name);
this.ratio = ratio;
this.dailyLimit = dailyLimit;
Expand All @@ -63,7 +69,7 @@ public float getRatio() {
return ratio;
}

public String getDailyLimit() {
public int getDailyLimit() {
return dailyLimit;
}

Expand All @@ -82,5 +88,10 @@ public boolean isIncludedByRatio(float percentile) {
public boolean isExcludedByRatio(float percentile) {
return 1 - ratio < percentile;
}

@Override
public boolean isWithinDailyLimit(int total) {
return total < dailyLimit; // at least 1 more to go
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ float getRatio() {
return ratio;
}

public boolean canSend(Event event) {
public boolean canSend(Event event, int currentTotal) {
if (event == null) {
return false;
}
Expand All @@ -71,7 +71,7 @@ public boolean canSend(Event event) {
return false;
}

return isIncluded(event)
return isIncluded(event, currentTotal)
&& !isExcluded(event);
}

Expand Down Expand Up @@ -99,13 +99,14 @@ List<Filter> getIncludes() {
return includes;
}

boolean isIncluded(Event event) {
boolean isIncluded(Event event, int currentTotal) {
Filter matching = includes.stream()
.filter(filter -> filter.isMatching(event))
.findAny()
.orElse(null);
return matching == null
|| matching.isIncludedByRatio(userId.getPercentile());
return matching == null ||
(matching.isIncludedByRatio(userId.getPercentile())
&& matching.isWithinDailyLimit(currentTotal));
}

boolean isExcluded(Event event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventNameFilter;
import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventPropertyFilter;
import com.redhat.devtools.intellij.telemetry.core.service.TelemetryService;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
Expand All @@ -34,6 +36,8 @@

class PluginLimitsDeserialization extends StdDeserializer<List<PluginLimits>> {

private static final Logger LOGGER = Logger.getInstance(PluginLimitsDeserialization.class);

public static final String FIELDNAME_ENABLED = "enabled";
public static final String FIELDNAME_REFRESH = "refresh";
public static final String FIELDNAME_RATIO = "ratio";
Expand All @@ -44,6 +48,8 @@ class PluginLimitsDeserialization extends StdDeserializer<List<PluginLimits>> {
public static final String FIELDNAME_DAILY_LIMIT = "dailyLimit";
public static final String FIELDNAME_NAME = "name";

public static final int DEFAULT_NUMERIC_VALUE = -1;

public static List<PluginLimits> create(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
Expand Down Expand Up @@ -88,14 +94,14 @@ private Enabled getEnabled(JsonNode node) {
}

private int getRefresh(JsonNode node) {
int numeric = -1;
int numeric = DEFAULT_NUMERIC_VALUE;
if (node != null) {
String refresh = getNumericPortion(node.asText().toCharArray());
if (!StringUtil.isEmptyOrSpaces(refresh)) {
try {
numeric = Integer.parseInt(refresh);
} catch (NumberFormatException e) {
// swallow
LOGGER.warn("Could not convert " + FIELDNAME_REFRESH + " to integer value: " + refresh);
}
}
}
Expand Down Expand Up @@ -128,7 +134,7 @@ private Filter createMessageLimitFilter(JsonNode node) {
private EventNameFilter createEventNameFilter(JsonNode node) {
String name = getStringValue(FIELDNAME_NAME, node);
float ratio = getRatio(node.get(FIELDNAME_RATIO));
String dailyLimit = getStringValue(FIELDNAME_DAILY_LIMIT, node);
int dailyLimit = getIntValue(FIELDNAME_DAILY_LIMIT, node);
return new EventNameFilter(name, ratio, dailyLimit);
}

Expand Down Expand Up @@ -158,6 +164,15 @@ private static String getStringValue(String name, JsonNode node) {
return node.get(name).asText();
}

private static int getIntValue(String name, JsonNode node) {
int numeric = DEFAULT_NUMERIC_VALUE;
if (node != null
&& node.get(name) == null) {
numeric = node.get(name).asInt(DEFAULT_NUMERIC_VALUE);
}
return numeric;
}

private static String getNumericPortion(char[] characters) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < characters.length && Character.isDigit(characters[i]); i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void getAllLimits_should_return_null_if_deserialization_throws() throws I
@Test
public void getAllLimits_should_download_remote_if_local_file_has_no_modification_timestamp() {
// given
List<PluginLimits> allLimits = List.of(mockDefaultPluginLimitsWithRefresh(Integer.MAX_VALUE));
List<PluginLimits> allLimits = List.of(createDefaultPluginLimits(Integer.MAX_VALUE));
Configurations configurations = mock(Configurations.class);
doReturn(null) // no modification timestamp, file does not exist
.when(configurations).getLocalLastModified();
Expand Down Expand Up @@ -82,7 +82,7 @@ public void getAllLimits_should_download_remote_if_local_file_was_modified_7h_ag
@Test
public void getAllLimits_should_download_remote_if_local_file_was_modified_7h_ago_and_default_limits_has_no_refresh() {
// given
List<PluginLimits> allLimits = List.of(mockDefaultPluginLimitsWithRefresh(-1));
List<PluginLimits> allLimits = List.of(createDefaultPluginLimits(-1));
PluginLimitsFactory factory = mock(PluginLimitsFactory.class);
Configurations configurations = mock(Configurations.class);
// default refresh (with plugin limits without refresh) is 6h
Expand All @@ -99,7 +99,7 @@ public void getAllLimits_should_download_remote_if_local_file_was_modified_7h_ag
@Test
public void getAllLimits_should_NOT_download_remote_if_local_file_was_modified_within_specified_refresh_period() {
// given
List<PluginLimits> allLimits = List.of(mockDefaultPluginLimitsWithRefresh(2));
List<PluginLimits> allLimits = List.of(createDefaultPluginLimits(2));
PluginLimitsFactory factory = mock(PluginLimitsFactory.class);
Configurations configurations = mock(Configurations.class);
doReturn(createFileTime(1)) // 1h ago
Expand Down Expand Up @@ -132,7 +132,7 @@ private static FileTime createFileTime(int createdHoursAgo) {
Instant.now().minus(createdHoursAgo, ChronoUnit.HOURS));
}

private static PluginLimits mockDefaultPluginLimitsWithRefresh(int refresh) {
private static PluginLimits createDefaultPluginLimits(int refresh) {
PluginLimits pluginLimit = mock(PluginLimits.class);
doReturn(refresh)
.when(pluginLimit).getRefresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class EventNameFilterTest {
@Test
public void isMatching_should_match_event_name() {
// given
Filter filter = new EventNameFilter("yoda", 0.42f, "42");
Filter filter = new EventNameFilter("yoda", 0.42f, 42);
Event event = new Event(Event.Type.USER, "yoda");
// when
boolean matching = filter.isMatching(event);
Expand All @@ -35,7 +35,7 @@ public void isMatching_should_match_event_name() {
@Test
public void isMatching_should_NOT_match_event_name_that_is_different() {
// given
Filter filter = new EventNameFilter("yoda", 0.42f, "42");
Filter filter = new EventNameFilter("yoda", 0.42f, 42);
Event event = new Event(Event.Type.USER, "darthvader");
// when
boolean matching = filter.isMatching(event);
Expand All @@ -46,7 +46,7 @@ public void isMatching_should_NOT_match_event_name_that_is_different() {
@Test
public void isMatching_should_match_event_name_when_pattern_is_wildcard() {
// given
Filter filter = new EventNameFilter("*", 0.42f, "42");
Filter filter = new EventNameFilter("*", 0.42f, 42);
Event event = new Event(Event.Type.USER, "skywalker");
// when
boolean matching = filter.isMatching(event);
Expand All @@ -57,7 +57,7 @@ public void isMatching_should_match_event_name_when_pattern_is_wildcard() {
@Test
public void isMatching_should_match_event_name_when_pattern_has_name_with_wildcards() {
// given
Filter filter = new EventNameFilter("*walk*", 0.42f, "42");
Filter filter = new EventNameFilter("*walk*", 0.42f, 42);
Event event = new Event(Event.Type.USER, "skywalker");
// when
boolean matching = filter.isMatching(event);
Expand Down
Loading

0 comments on commit e9fbdd0

Please sign in to comment.