Skip to content

Commit d5d7ce0

Browse files
committed
fix(api): harden load rejection logging
1 parent 38d8a66 commit d5d7ce0

2 files changed

Lines changed: 148 additions & 15 deletions

File tree

hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/LoadDetectFilter.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,24 @@ public static boolean isWhiteAPI(ContainerRequestContext context) {
7474
return WHITE_API_LIST.contains(rootPath);
7575
}
7676

77-
private static boolean gcIfNeeded() {
77+
protected boolean gcIfNeeded() {
7878
if (GC_RATE_LIMITER.tryAcquire(1)) {
7979
System.gc();
8080
return true;
8181
}
8282
return false;
8383
}
8484

85+
protected boolean allowRejectLog() {
86+
return REJECT_LOG_RATE_LIMITER.tryAcquire();
87+
}
88+
89+
protected void logRejectWarning(String message, Object... args) {
90+
if (this.allowRejectLog()) {
91+
LOG.warn(message, args);
92+
}
93+
}
94+
8595
@Override
8696
public void filter(ContainerRequestContext context) {
8797
if (LoadDetectFilter.isWhiteAPI(context)) {
@@ -95,12 +105,10 @@ public void filter(ContainerRequestContext context) {
95105
// There will be a thread doesn't work, dedicated to statistics
96106
int currentLoad = load.incrementAndGet();
97107
if (currentLoad >= maxWorkerThreads) {
98-
if (REJECT_LOG_RATE_LIMITER.tryAcquire()) {
99-
LOG.warn("Rejected request due to high worker load, method={}, path={}, " +
100-
"currentLoad={}, maxWorkerThreads={}",
101-
context.getMethod(), context.getUriInfo().getPath(),
102-
currentLoad, maxWorkerThreads);
103-
}
108+
this.logRejectWarning("Rejected request due to high worker load, method={}, path={}, " +
109+
"currentLoad={}, maxWorkerThreads={}",
110+
context.getMethod(), context.getUriInfo().getPath(),
111+
currentLoad, maxWorkerThreads);
104112
throw new ServiceUnavailableException(String.format(
105113
"The server is too busy to process the request, " +
106114
"you can config %s to adjust it or try again later",
@@ -113,17 +121,17 @@ public void filter(ContainerRequestContext context) {
113121
long presumableFreeMem = (Runtime.getRuntime().maxMemory() -
114122
allocatedMem) / Bytes.MB;
115123
if (presumableFreeMem < minFreeMemory) {
116-
boolean gcTriggered = gcIfNeeded();
124+
boolean gcTriggered = this.gcIfNeeded();
117125
long allocatedMemAfterCheck = Runtime.getRuntime().totalMemory() -
118126
Runtime.getRuntime().freeMemory();
119127
long recheckedFreeMem = (Runtime.getRuntime().maxMemory() -
120128
allocatedMemAfterCheck) / Bytes.MB;
121-
LOG.warn("Rejected request due to low free memory, method={}, path={}, " +
122-
"presumableFreeMemMB={}, recheckedFreeMemMB={}, gcTriggered={}, " +
123-
"minFreeMemoryMB={}",
124-
context.getMethod(), context.getUriInfo().getPath(),
125-
presumableFreeMem, recheckedFreeMem, gcTriggered,
126-
minFreeMemory);
129+
this.logRejectWarning("Rejected request due to low free memory, method={}, path={}, " +
130+
"presumableFreeMemMB={}, recheckedFreeMemMB={}, gcTriggered={}, " +
131+
"minFreeMemoryMB={}",
132+
context.getMethod(), context.getUriInfo().getPath(),
133+
presumableFreeMem, recheckedFreeMem, gcTriggered,
134+
minFreeMemory);
127135
throw new ServiceUnavailableException(String.format(
128136
"The server available memory %s(MB) is below than " +
129137
"threshold %s(MB) and can't process the request, " +

hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/LoadDetectFilterTest.java

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
package org.apache.hugegraph.unit.api.filter;
1919

20+
import java.io.Serializable;
21+
import java.util.ArrayDeque;
22+
import java.util.ArrayList;
23+
import java.util.Deque;
2024
import java.util.List;
2125
import java.util.stream.Collectors;
2226

@@ -29,6 +33,15 @@
2933
import org.apache.hugegraph.testutil.Assert;
3034
import org.apache.hugegraph.testutil.Whitebox;
3135
import org.apache.hugegraph.unit.BaseUnitTest;
36+
import org.apache.logging.log4j.Level;
37+
import org.apache.logging.log4j.LogManager;
38+
import org.apache.logging.log4j.core.Filter;
39+
import org.apache.logging.log4j.core.Layout;
40+
import org.apache.logging.log4j.core.LogEvent;
41+
import org.apache.logging.log4j.core.Logger;
42+
import org.apache.logging.log4j.core.appender.AbstractAppender;
43+
import org.apache.logging.log4j.core.config.Property;
44+
import org.junit.After;
3245
import org.junit.Before;
3346
import org.junit.Test;
3447
import org.mockito.Mockito;
@@ -41,25 +54,38 @@
4154

4255
public class LoadDetectFilterTest extends BaseUnitTest {
4356

57+
private static final Logger TEST_LOGGER =
58+
(Logger) LogManager.getLogger(LoadDetectFilter.class);
59+
4460
private LoadDetectFilter loadDetectFilter;
4561
private ContainerRequestContext requestContext;
4662
private UriInfo uriInfo;
4763
private WorkLoad workLoad;
64+
private TestAppender testAppender;
4865

4966
@Before
5067
public void setup() {
5168
this.requestContext = Mockito.mock(ContainerRequestContext.class);
5269
this.uriInfo = Mockito.mock(UriInfo.class);
5370
this.workLoad = new WorkLoad();
71+
this.testAppender = new TestAppender();
72+
this.testAppender.start();
73+
TEST_LOGGER.addAppender(this.testAppender);
5474

5575
Mockito.when(this.requestContext.getUriInfo()).thenReturn(this.uriInfo);
5676
Mockito.when(this.requestContext.getMethod()).thenReturn("GET");
5777

58-
this.loadDetectFilter = new LoadDetectFilter();
78+
this.loadDetectFilter = new TestLoadDetectFilter();
5979
this.setLoadProvider(this.workLoad);
6080
this.setConfigProvider(createConfig(8, 0));
6181
}
6282

83+
@After
84+
public void teardown() {
85+
TEST_LOGGER.removeAppender(this.testAppender);
86+
this.testAppender.stop();
87+
}
88+
6389
@Test
6490
public void testFilter_WhiteListPathIgnored() {
6591
setupPath("", List.of(""));
@@ -68,6 +94,7 @@ public void testFilter_WhiteListPathIgnored() {
6894
this.loadDetectFilter.filter(this.requestContext);
6995

7096
Assert.assertEquals(1, this.workLoad.get().get());
97+
Assert.assertTrue(this.testAppender.events().isEmpty());
7198
}
7299

73100
@Test
@@ -85,13 +112,19 @@ public void testFilter_RejectsWhenWorkerLoadIsTooHigh() {
85112
exception.getMessage());
86113
Assert.assertContains(ServerOptions.MAX_WORKER_THREADS.name(),
87114
exception.getMessage());
115+
Assert.assertEquals(1, this.testAppender.events().size());
116+
this.assertWarnLogContains("Rejected request due to high worker load");
117+
this.assertWarnLogContains("method=GET");
118+
this.assertWarnLogContains("path=graphs/hugegraph/vertices");
119+
this.assertWarnLogContains("currentLoad=2");
88120
}
89121

90122
@Test
91123
public void testFilter_RejectsWhenFreeMemoryIsTooLow() {
92124
setupPath("graphs/hugegraph/vertices",
93125
List.of("graphs", "hugegraph", "vertices"));
94126
this.setConfigProvider(createConfig(8, Integer.MAX_VALUE));
127+
this.setGcTriggered(false);
95128

96129
ServiceUnavailableException exception = (ServiceUnavailableException) Assert.assertThrows(
97130
ServiceUnavailableException.class,
@@ -101,6 +134,11 @@ public void testFilter_RejectsWhenFreeMemoryIsTooLow() {
101134
exception.getMessage());
102135
Assert.assertContains(ServerOptions.MIN_FREE_MEMORY.name(),
103136
exception.getMessage());
137+
Assert.assertEquals(1, this.testAppender.events().size());
138+
this.assertWarnLogContains("Rejected request due to low free memory");
139+
this.assertWarnLogContains("method=GET");
140+
this.assertWarnLogContains("path=graphs/hugegraph/vertices");
141+
this.assertWarnLogContains("gcTriggered=false");
104142
}
105143

106144
@Test
@@ -112,6 +150,26 @@ public void testFilter_AllowsRequestWhenLoadAndMemoryAreHealthy() {
112150
this.loadDetectFilter.filter(this.requestContext);
113151

114152
Assert.assertEquals(1, this.workLoad.get().get());
153+
Assert.assertTrue(this.testAppender.events().isEmpty());
154+
}
155+
156+
@Test
157+
public void testFilter_RejectLogIsRateLimited() {
158+
setupPath("graphs/hugegraph/vertices",
159+
List.of("graphs", "hugegraph", "vertices"));
160+
this.setConfigProvider(createConfig(2, 0));
161+
this.setAllowRejectLogs(true, false);
162+
163+
this.workLoad.incrementAndGet();
164+
Assert.assertThrows(ServiceUnavailableException.class,
165+
() -> this.loadDetectFilter.filter(this.requestContext));
166+
167+
this.workLoad.get().set(1);
168+
Assert.assertThrows(ServiceUnavailableException.class,
169+
() -> this.loadDetectFilter.filter(this.requestContext));
170+
171+
Assert.assertEquals(1, this.testAppender.events().size());
172+
this.assertWarnLogContains("Rejected request due to high worker load");
115173
}
116174

117175
private HugeConfig createConfig(int maxWorkerThreads, int minFreeMemory) {
@@ -144,4 +202,71 @@ private void setConfigProvider(HugeConfig config) {
144202
Whitebox.setInternalState(this.loadDetectFilter, "configProvider",
145203
(Provider<HugeConfig>) () -> config);
146204
}
205+
206+
private void setGcTriggered(boolean gcTriggered) {
207+
((TestLoadDetectFilter) this.loadDetectFilter).gcTriggered(gcTriggered);
208+
}
209+
210+
private void setAllowRejectLogs(boolean... allowedLogs) {
211+
((TestLoadDetectFilter) this.loadDetectFilter).allowRejectLogs(allowedLogs);
212+
}
213+
214+
private void assertWarnLogContains(String expectedContent) {
215+
Assert.assertFalse(this.testAppender.events().isEmpty());
216+
LogEvent event = this.testAppender.events().get(0);
217+
Assert.assertEquals(Level.WARN, event.getLevel());
218+
Assert.assertContains(expectedContent,
219+
event.getMessage().getFormattedMessage());
220+
}
221+
222+
private static class TestLoadDetectFilter extends LoadDetectFilter {
223+
224+
private boolean gcTriggered;
225+
private final Deque<Boolean> allowRejectLogs = new ArrayDeque<>();
226+
227+
public void gcTriggered(boolean gcTriggered) {
228+
this.gcTriggered = gcTriggered;
229+
}
230+
231+
public void allowRejectLogs(boolean... allowedLogs) {
232+
this.allowRejectLogs.clear();
233+
for (boolean allowedLog : allowedLogs) {
234+
this.allowRejectLogs.addLast(allowedLog);
235+
}
236+
}
237+
238+
@Override
239+
protected boolean gcIfNeeded() {
240+
return this.gcTriggered;
241+
}
242+
243+
@Override
244+
protected boolean allowRejectLog() {
245+
if (this.allowRejectLogs.isEmpty()) {
246+
return true;
247+
}
248+
return this.allowRejectLogs.removeFirst();
249+
}
250+
}
251+
252+
private static class TestAppender extends AbstractAppender {
253+
254+
private final List<LogEvent> events;
255+
256+
protected TestAppender() {
257+
super("LoadDetectFilterTestAppender", (Filter) null,
258+
(Layout<? extends Serializable>) null, false,
259+
Property.EMPTY_ARRAY);
260+
this.events = new ArrayList<>();
261+
}
262+
263+
@Override
264+
public void append(LogEvent event) {
265+
this.events.add(event.toImmutable());
266+
}
267+
268+
public List<LogEvent> events() {
269+
return this.events;
270+
}
271+
}
147272
}

0 commit comments

Comments
 (0)