Skip to content

Commit 5f7eeca

Browse files
authored
feat(ratelimiter): add configurable blocking mode for API rate limiters (#6761)
- Add `rate.limiter.apiNonBlocking` switch (default false). When off, callers wait for a permit; when on, they reject immediately and shed load. - Wire the switch through `RateLimiterConfig` → `Args` → `CommonParameter`; update `reference.conf` / `config.conf`. - Add blocking `acquire()` paths on `IRateLimiter`, `GlobalRateLimiter`, and `QpsStrategy` / `IPQpsStrategy` / `GlobalPreemptibleStrategy`. Only the semaphore-based strategy is bounded (2s); the QPS-based paths use unbounded Guava `RateLimiter.acquire()`. - Introduce `acquirePermit()` dispatcher (default method on `IRateLimiter`; static on `GlobalRateLimiter`) that picks blocking vs non-blocking based on the switch. - Swap `tryAcquire` → `acquirePermit` at the `RateLimiterServlet` and `RateLimiterInterceptor` call sites; preserve per-endpoint-before-global ordering to avoid consuming global quota on per-endpoint rejection. - Extract `loadIpLimiter()` in `GlobalRateLimiter` and `loadLimiter()` in `IPQpsStrategy` to share cache+exception handling between `tryAcquire` and `acquire`. - Add unit tests covering dispatcher routing (both directions), acquire IP-before-global ordering, and IP-loader-failure fail-closed behaviour.
1 parent 9d28fa7 commit 5f7eeca

21 files changed

Lines changed: 314 additions & 52 deletions

File tree

common/src/main/java/org/tron/common/parameter/CommonParameter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ public class CommonParameter {
411411
@Setter
412412
public double rateLimiterDisconnect; // clearParam: 1.0
413413
@Getter
414+
@Setter
415+
public boolean rateLimiterApiNonBlocking = false;
416+
@Getter
414417
public RocksDbSettings rocksDBCustomSettings;
415418
@Getter
416419
public GenesisBlock genesisBlock;

common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class RateLimiterConfig {
2121
private P2pRateLimitConfig p2p = new P2pRateLimitConfig();
2222
private List<HttpRateLimitItem> http = new ArrayList<>();
2323
private List<RpcRateLimitItem> rpc = new ArrayList<>();
24+
private boolean apiNonBlocking = false;
2425

2526
@Getter
2627
@Setter

common/src/main/resources/reference.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ rate.limiter = {
451451
global.qps = 50000
452452
global.ip.qps = 10000
453453
global.api.qps = 1000
454+
apiNonBlocking = false
454455
}
455456

456457
seed.node = {

common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.tron.core.config.args;
22

33
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
45
import static org.junit.Assert.assertTrue;
56

67
import com.typesafe.config.Config;
@@ -29,6 +30,7 @@ public void testDefaults() {
2930
assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001);
3031
assertTrue(rl.getHttp().isEmpty());
3132
assertTrue(rl.getRpc().isEmpty());
33+
assertFalse(rl.isApiNonBlocking());
3234
}
3335

3436
@Test
@@ -40,7 +42,8 @@ public void testFromConfig() {
4042
+ " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter,"
4143
+ " paramString = \"qps=10\" }],"
4244
+ " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter,"
43-
+ " paramString = \"permit=1\" }]"
45+
+ " paramString = \"permit=1\" }],"
46+
+ " apiNonBlocking = true"
4447
+ "}");
4548
RateLimiterConfig rl = RateLimiterConfig.fromConfig(config);
4649
assertEquals(100, rl.getGlobal().getQps());
@@ -50,5 +53,6 @@ public void testFromConfig() {
5053
assertEquals("TestServlet", rl.getHttp().get(0).getComponent());
5154
assertEquals(1, rl.getRpc().size());
5255
assertEquals("TestRpc", rl.getRpc().get(0).getComponent());
56+
assertTrue(rl.isApiNonBlocking());
5357
}
5458
}

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ private static void applyRateLimiterConfig(RateLimiterConfig rl) {
328328
PARAMETER.rateLimiterSyncBlockChain = rl.getP2p().getSyncBlockChain();
329329
PARAMETER.rateLimiterFetchInvData = rl.getP2p().getFetchInvData();
330330
PARAMETER.rateLimiterDisconnect = rl.getP2p().getDisconnect();
331+
PARAMETER.rateLimiterApiNonBlocking = rl.isApiNonBlocking();
331332

332333
// HTTP/RPC rate limiter items: convert bean lists to business objects
333334
RateLimiterInitialization initialization = new RateLimiterInitialization();

framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
107107
IRateLimiter rateLimiter = container.get(KEY_PREFIX_HTTP, getClass().getSimpleName());
108108

109109
// Check per-endpoint first to avoid consuming global IP/QPS quota for requests
110-
// that would be rejected by the per-endpoint limiter anyway.
111-
boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData);
112-
boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData);
110+
// that would be rejected by the per-endpoint limiter anyway. acquirePermit()
111+
// chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking.
112+
boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData);
113+
boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData);
113114

114115
String contextPath = req.getContextPath();
115116
String url = Strings.isNullOrEmpty(req.getServletPath())

framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,43 @@ public class GlobalRateLimiter {
2323
public static boolean tryAcquire(RuntimeData runtimeData) {
2424
String ip = runtimeData.getRemoteAddr();
2525
if (!Strings.isNullOrEmpty(ip)) {
26-
RateLimiter r;
27-
try {
28-
// cache.get is atomic: only one loader executes per key under concurrent requests,
29-
// preventing multiple RateLimiter instances from being created for the same IP.
30-
r = cache.get(ip, () -> RateLimiter.create(IP_QPS));
31-
} catch (Exception e) {
32-
logger.warn("Failed to load IP rate limiter for {}, denying request: {}",
33-
ip, e.getMessage());
26+
RateLimiter r = loadIpLimiter(ip);
27+
if (r == null || !r.tryAcquire()) {
3428
return false;
3529
}
36-
if (!r.tryAcquire()) {
30+
}
31+
return rateLimiter.tryAcquire();
32+
}
33+
34+
public static boolean acquire(RuntimeData runtimeData) {
35+
String ip = runtimeData.getRemoteAddr();
36+
if (!Strings.isNullOrEmpty(ip)) {
37+
RateLimiter r = loadIpLimiter(ip);
38+
if (r == null) {
3739
return false;
3840
}
41+
r.acquire();
42+
}
43+
rateLimiter.acquire();
44+
return true;
45+
}
46+
47+
public static boolean acquirePermit(RuntimeData runtimeData) {
48+
return Args.getInstance().isRateLimiterApiNonBlocking()
49+
? tryAcquire(runtimeData)
50+
: acquire(runtimeData);
51+
}
52+
53+
private static RateLimiter loadIpLimiter(String ip) {
54+
try {
55+
// cache.get is atomic: only one loader executes per key under concurrent requests,
56+
// preventing multiple RateLimiter instances from being created for the same IP.
57+
return cache.get(ip, () -> RateLimiter.create(IP_QPS));
58+
} catch (Exception e) {
59+
logger.warn("Failed to load IP rate limiter for {}, denying request: {}",
60+
ip, e.getMessage());
61+
return null;
3962
}
40-
return rateLimiter.tryAcquire();
4163
}
4264

4365
}

framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
108108

109109
RuntimeData runtimeData = new RuntimeData(call);
110110
// Check per-endpoint first to avoid consuming global IP/QPS quota for requests
111-
// that would be rejected by the per-endpoint limiter anyway.
112-
boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData);
113-
boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData);
111+
// that would be rejected by the per-endpoint limiter anyway. acquirePermit()
112+
// chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking.
113+
boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData);
114+
boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData);
114115

115116
if (!acquireResource) {
116117
// Release the per-endpoint permit when global rejected, to avoid semaphore leak.

framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public DefaultBaseQqsAdapter(String paramString) {
1515
public boolean tryAcquire(RuntimeData data) {
1616
return strategy.tryAcquire();
1717
}
18+
19+
@Override
20+
public boolean acquire(RuntimeData data) {
21+
return strategy.acquire();
22+
}
1823
}

framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ public boolean tryAcquire(RuntimeData data) {
2121
return strategy.tryAcquire();
2222
}
2323

24+
@Override
25+
public boolean acquire(RuntimeData data) {
26+
return strategy.acquire();
27+
}
2428
}

0 commit comments

Comments
 (0)