Skip to content

Commit fdb3f76

Browse files
authored
Merge pull request #11424 from IQSS/11413-rate-limiting-statistics-api
Adding rate limiting statistics api
2 parents c1c06e3 + e6473e3 commit fdb3f76

File tree

8 files changed

+143
-7
lines changed

8 files changed

+143
-7
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Rate Limiting Statistics API
2+
3+
CSV dump of Rate Limiting Cache
4+
5+
curl http://localhost:8080/api/admin/rateLimitStats
6+
curl http://localhost:8080/api/admin/rateLimitStats?deltaMinutesFilter=10
7+
8+
See also [the guides](https://dataverse-guide--11359.org.readthedocs.build/en/11359/admin/rate_limiting.html), #11413, and #11359.

doc/sphinx-guides/source/admin/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ This guide documents the functionality only available to superusers (such as "da
3030
mail-groups
3131
collectionquotas
3232
monitoring
33+
rate-limiting
3334
reporting-tools-and-queries
3435
maintenance
3536
backups
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Rate Limiting
2+
=============
3+
4+
.. contents:: Contents:
5+
:local:
6+
7+
Configuration
8+
-------------
9+
10+
Rate limiting is used to prevent users from over taxing the system either deliberately or by runaway automated processes.
11+
Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. New users are defaulted to tier 1.
12+
Superuser accounts are exempt from rate limiting.
13+
Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database.
14+
Two database settings configure the rate limiting.
15+
Note: If either of these settings exist in the database rate limiting will be enabled (note that a Payara restart is required for the :RateLimitingDefaultCapacityTiers setting to take effect). If neither setting exists rate limiting is disabled.
16+
17+
- :RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,...
18+
A value of -1 can be used to signify no rate limit. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..."
19+
20+
.. code-block:: bash
21+
22+
curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'
23+
24+
- :RateLimitingCapacityByTierAndAction is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls.
25+
In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API.
26+
27+
:download:`rate-limit-actions.json </_static/installation/files/examples/rate-limit-actions-setting.json>` Example JSON for RateLimitingCapacityByTierAndAction
28+
29+
.. code-block:: bash
30+
31+
curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'
32+
33+
Statistics
34+
----------
35+
36+
In order to monitor the rate limiting cache for investigative purposes there is a stats endpoint which returns CSV formatted text.
37+
38+
The CSV contains multiple lists.
39+
40+
- The first list contains the username:command, and number of tokens remaining. This list is sorted by the values and has the header "#<username>:<command>, <available tokens>".
41+
42+
- The second list contains username:command, last updated timestamp in minutes, and delta minutes before now. The header for this list is "#<username>:<command>, <timestamp>, <delta minutes> ## deltaMinutesFilter=1". This list can be filtered to show only the entries with updates within the deltaMinutesFilter requested. ("## deltaMinutesFilter=n" will be added to the header when the filter is included in the call.
43+
44+
.. code-block:: bash
45+
46+
curl http://localhost:8080/api/admin/rateLimitStats
47+
curl http://localhost:8080/api/admin/rateLimitStats?deltaMinutesFilter=10

src/main/java/edu/harvard/iq/dataverse/api/Admin.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
2020
import edu.harvard.iq.dataverse.settings.JvmSettings;
2121
import edu.harvard.iq.dataverse.util.StringUtil;
22+
import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean;
23+
import edu.harvard.iq.dataverse.util.cache.RateLimitUtil;
2224
import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder;
2325
import edu.harvard.iq.dataverse.validation.EMailValidator;
2426
import edu.harvard.iq.dataverse.EjbDataverseEngine;
@@ -183,6 +185,8 @@ public class Admin extends AbstractApiBean {
183185
BannerMessageServiceBean bannerMessageService;
184186
@EJB
185187
TemplateServiceBean templateService;
188+
@EJB
189+
CacheFactoryBean cacheFactory;
186190

187191
// Make the session available
188192
@Inject
@@ -2717,4 +2721,22 @@ public Response getAuditFiles(@Context ContainerRequestContext crc,
27172721

27182722
return ok(jsonObjectBuilder);
27192723
}
2724+
2725+
@GET
2726+
@AuthRequired
2727+
@Path("/rateLimitStats")
2728+
@Produces("text/csv")
2729+
public Response rateLimitStats(@Context ContainerRequestContext crc,
2730+
@QueryParam("deltaMinutesFilter") Long deltaMinutesFilter) {
2731+
try {
2732+
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
2733+
if (!user.isSuperuser()) {
2734+
return error(Response.Status.FORBIDDEN, "Superusers only.");
2735+
}
2736+
} catch (WrappedResponse wr) {
2737+
return wr.getResponse();
2738+
}
2739+
String csvData = cacheFactory.getStats(CacheFactoryBean.RATE_LIMIT_CACHE, deltaMinutesFilter != null ? String.valueOf(deltaMinutesFilter) : null);
2740+
return Response.ok(csvData).header("Content-Disposition", "attachment; filename=\"data.csv\"").build();
2741+
}
27202742
}

src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import javax.cache.CacheManager;
1414
import javax.cache.configuration.CompleteConfiguration;
1515
import javax.cache.configuration.MutableConfiguration;
16-
import javax.cache.spi.CachingProvider;
1716
import java.util.logging.Logger;
1817

1918
@Singleton
@@ -26,8 +25,6 @@ public class CacheFactoryBean implements java.io.Serializable {
2625
SystemConfig systemConfig;
2726
@Inject
2827
CacheManager manager;
29-
@Inject
30-
CachingProvider provider;
3128
public final static String RATE_LIMIT_CACHE = "rateLimitCache";
3229

3330
@PostConstruct
@@ -60,4 +57,10 @@ public boolean checkRate(User user, Command command) {
6057
return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity));
6158
}
6259
}
60+
public String getStats(String cacheType, String filter) {
61+
if (RATE_LIMIT_CACHE.equals(cacheType)) {
62+
return RateLimitUtil.getStats(rateLimitCache, filter);
63+
}
64+
return "";
65+
}
6366
}

src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
import jakarta.json.bind.JsonbException;
1010

1111
import javax.cache.Cache;
12-
import java.util.ArrayList;
13-
import java.util.Arrays;
14-
import java.util.List;
15-
import java.util.Map;
12+
import java.util.*;
1613
import java.util.concurrent.ConcurrentHashMap;
1714
import java.util.concurrent.CopyOnWriteArrayList;
1815
import java.util.logging.Logger;
@@ -132,6 +129,33 @@ static void getRateLimitsFromJson(SystemConfig systemConfig) {
132129
}
133130
}
134131
}
132+
static String getStats(final Cache<String, String> rateLimitCache, String filter) {
133+
StringBuilder sb1 = new StringBuilder(); // available tokens list
134+
StringBuilder sb2 = new StringBuilder(); // updated cache list
135+
long currentTime = System.currentTimeMillis() / 60000L; // convert to minutes
136+
Long deltaMinutesFilter = filter != null ? Long.parseLong(filter) : null;
137+
Iterator<Cache.Entry<String, String>> iterator = rateLimitCache.iterator();
138+
sb1.append("#<username>:<command>, <available tokens>\n");
139+
sb2.append("#<username>:<command>, <timestamp>, <delta minutes>");
140+
sb2.append(deltaMinutesFilter != null ? " ## deltaMinutesFilter=" + deltaMinutesFilter + "\n" : "\n");
141+
int cacheEntries = 0;
142+
143+
while (iterator.hasNext()) {
144+
Cache.Entry<String, String> entry = iterator.next();
145+
if (entry.getKey().endsWith(":last_update")) {
146+
long deltaMinutes = currentTime - Long.parseLong(String.valueOf(entry.getValue()));
147+
if (deltaMinutesFilter == null || deltaMinutes <= deltaMinutesFilter) {
148+
sb2.append(entry.getKey() + ", " + entry.getValue() + ", " + deltaMinutes + "\n");
149+
}
150+
} else {
151+
sb1.append(entry.getKey() + ", " + entry.getValue() + "\n");
152+
cacheEntries++;
153+
}
154+
}
155+
156+
String header = "# Rate Limit Cache: Total number of cached entries (excluding \":last_update\") " + cacheEntries + "\n";
157+
return header + sortString(sb1) + sortString(sb2);
158+
}
135159
static String getMapKey(int tier) {
136160
return getMapKey(tier, null);
137161
}
@@ -142,4 +166,9 @@ static long longFromKey(Cache<String, String> cache, String key) {
142166
Object l = cache.get(key);
143167
return l != null ? Long.parseLong(String.valueOf(l)) : 0L;
144168
}
169+
static String sortString(StringBuilder sb) {
170+
String[] strings = sb.toString().split("\\R");
171+
Arrays.sort(strings);
172+
return String.join("\n", strings) + "\n";
173+
}
145174
}

src/test/java/edu/harvard/iq/dataverse/api/SendFeedbackApiIT.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ private void validateEmail(String body) {
192192

193193
@Test
194194
public void testSendRateLimited() {
195+
Response createSuperuser = UtilIT.createRandomUser();
196+
String superuserUsername = UtilIT.getUsernameFromResponse(createSuperuser);
197+
String superuserApiToken = UtilIT.getApiTokenFromResponse(createSuperuser);
198+
Response toggleSuperuser = UtilIT.makeSuperUser(superuserUsername);
199+
toggleSuperuser.then().assertThat()
200+
.statusCode(OK.getStatusCode());
201+
195202
Response createUser = UtilIT.createRandomUser();
196203
createUser.prettyPrint();
197204
createUser.then().assertThat()
@@ -234,6 +241,19 @@ public void testSendRateLimited() {
234241
response.then().assertThat()
235242
.statusCode(TOO_MANY_REQUESTS.getStatusCode());
236243

244+
response = UtilIT.rateLimitStats(apiToken, null);
245+
response.prettyPrint();
246+
response.then().assertThat()
247+
.statusCode(FORBIDDEN.getStatusCode());
248+
response = UtilIT.rateLimitStats(superuserApiToken, null);
249+
response.prettyPrint();
250+
response.then().assertThat()
251+
.statusCode(OK.getStatusCode());
252+
response = UtilIT.rateLimitStats(superuserApiToken, "1");
253+
response.prettyPrint();
254+
response.then().assertThat()
255+
.statusCode(OK.getStatusCode());
256+
237257
response = UtilIT.sendFeedback(buildJsonEmail(datasetId, null, null), apiToken);
238258
response.prettyPrint();
239259
response.then().assertThat()

src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3032,6 +3032,12 @@ static Response clearMetricCache(String name) {
30323032
return requestSpecification.delete("/api/admin/clearMetricsCache/" + name);
30333033
}
30343034

3035+
static Response rateLimitStats(String apiToken, String deltaMinutesFilter) {
3036+
String queryParams = deltaMinutesFilter != null ? "?deltaMinutesFilter=" + deltaMinutesFilter : "";
3037+
return given()
3038+
.header(API_TOKEN_HTTP_HEADER, apiToken)
3039+
.get("/api/admin/rateLimitStats" + queryParams);
3040+
}
30353041

30363042
static Response sitemapUpdate() {
30373043
return given()

0 commit comments

Comments
 (0)