Skip to content

Commit 9a3f8ad

Browse files
Add settings to control the max size and count of warning headers in responses
Add a dynamic persistent cluster level setting "http.max_warning_header_count" to control the maximum number of warning headers in client HTTP responses. Defaults to unbounded. Add a dynamic persistent cluster level setting "http.max_warning_header_size" to control the maximum total size of warning headers in client HTTP responses. Defaults to unbounded. Closes #28301
1 parent 7d434c1 commit 9a3f8ad

File tree

6 files changed

+171
-4
lines changed

6 files changed

+171
-4
lines changed

docs/reference/modules/cluster/misc.asciidoc

+21
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,24 @@ PUT /_cluster/settings
5656
}
5757
-------------------------------
5858
// CONSOLE
59+
60+
[[cluster-warning-headers]]
61+
==== Warning Headers
62+
For every distinct warning, elastic cluster will return a new warning header in the client HTTP response.
63+
Sometimes the amount of returned warning headers can be too large and exceed client configuration settings.
64+
These dynamic settings allow to set the maximum count and size of warning headers in client http responses.
65+
Once the maximum count or size is reached, any extra warning will not produce an additional warning header.
66+
The default value for `http.max_warning_header_count` is unbounded.
67+
The default value for `http.max_warning_header_size` is unbounded.
68+
69+
[source,js]
70+
-------------------------------
71+
PUT /_cluster/settings
72+
{
73+
"persistent" : {
74+
"http.max_warning_header_count" : 62,
75+
"http.max_warning_header_size" : "7Kb"
76+
}
77+
}
78+
-------------------------------
79+
// CONSOLE

server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java

+2
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ public void apply(Settings value, Settings current, Settings previous) {
245245
HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH,
246246
HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE,
247247
HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE,
248+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT,
249+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE,
248250
HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH,
249251
HttpTransportSettings.SETTING_HTTP_READ_TIMEOUT,
250252
HttpTransportSettings.SETTING_HTTP_RESET_COOKIES,

server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

+81-3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@
2323
import org.elasticsearch.common.io.stream.StreamInput;
2424
import org.elasticsearch.common.io.stream.StreamOutput;
2525
import org.elasticsearch.common.io.stream.Writeable;
26+
import org.elasticsearch.common.logging.DeprecationLogger;
2627
import org.elasticsearch.common.logging.ESLoggerFactory;
2728
import org.elasticsearch.common.settings.Setting;
2829
import org.elasticsearch.common.settings.Setting.Property;
2930
import org.elasticsearch.common.settings.Settings;
31+
import org.elasticsearch.common.unit.ByteSizeValue;
32+
import org.elasticsearch.http.HttpTransportSettings;
33+
34+
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT;
35+
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE;
3036

3137
import java.io.Closeable;
3238
import java.io.IOException;
@@ -39,13 +45,14 @@
3945
import java.util.Set;
4046
import java.util.concurrent.CancellationException;
4147
import java.util.concurrent.ExecutionException;
42-
import java.util.concurrent.FutureTask;
4348
import java.util.concurrent.RunnableFuture;
4449
import java.util.concurrent.atomic.AtomicBoolean;
4550
import java.util.function.Function;
4651
import java.util.function.Supplier;
4752
import java.util.stream.Collectors;
4853
import java.util.stream.Stream;
54+
import java.nio.charset.StandardCharsets;
55+
4956

5057
/**
5158
* A ThreadContext is a map of string headers and a transient map of keyed objects that are associated with
@@ -81,6 +88,8 @@ public final class ThreadContext implements Closeable, Writeable {
8188
private static final ThreadContextStruct DEFAULT_CONTEXT = new ThreadContextStruct();
8289
private final Map<String, String> defaultHeader;
8390
private final ContextThreadLocal threadLocal;
91+
private static volatile int maxWrnHeaderCount;
92+
private static volatile long maxWrnHeaderSize;
8493

8594
/**
8695
* Creates a new ThreadContext instance
@@ -98,13 +107,23 @@ public ThreadContext(Settings settings) {
98107
this.defaultHeader = Collections.unmodifiableMap(defaultHeader);
99108
}
100109
threadLocal = new ContextThreadLocal();
110+
maxWrnHeaderCount = SETTING_HTTP_MAX_WARNING_HEADER_COUNT.get(settings);
111+
maxWrnHeaderSize = SETTING_HTTP_MAX_WARNING_HEADER_SIZE.get(settings).getBytes();
101112
}
102113

103114
@Override
104115
public void close() throws IOException {
105116
threadLocal.close();
106117
}
107118

119+
public static void setMaxWarningHeaderCount(int newMaxWrnHeaderCount){
120+
maxWrnHeaderCount = newMaxWrnHeaderCount;
121+
}
122+
123+
public static void setMaxWarningHeaderSize(ByteSizeValue newMaxWarningHeaderSize){
124+
maxWrnHeaderSize = newMaxWarningHeaderSize.getBytes();
125+
}
126+
108127
/**
109128
* Removes the current context and resets a default context. The removed context can be
110129
* restored when closing the returned {@link StoredContext}
@@ -359,7 +378,8 @@ private static final class ThreadContextStruct {
359378
private final Map<String, Object> transientHeaders;
360379
private final Map<String, List<String>> responseHeaders;
361380
private final boolean isSystemContext;
362-
381+
private long wrnHeadersSize; //saving current warning headers' size not to recalculate the size with every new warning header
382+
private boolean isWrnLmtReached;
363383
private ThreadContextStruct(StreamInput in) throws IOException {
364384
final int numRequest = in.readVInt();
365385
Map<String, String> requestHeaders = numRequest == 0 ? Collections.emptyMap() : new HashMap<>(numRequest);
@@ -371,6 +391,8 @@ private ThreadContextStruct(StreamInput in) throws IOException {
371391
this.responseHeaders = in.readMapOfLists(StreamInput::readString, StreamInput::readString);
372392
this.transientHeaders = Collections.emptyMap();
373393
isSystemContext = false; // we never serialize this it's a transient flag
394+
wrnHeadersSize = 0L;
395+
isWrnLmtReached = false;
374396
}
375397

376398
private ThreadContextStruct setSystemContext() {
@@ -387,6 +409,20 @@ private ThreadContextStruct(Map<String, String> requestHeaders,
387409
this.responseHeaders = responseHeaders;
388410
this.transientHeaders = transientHeaders;
389411
this.isSystemContext = isSystemContext;
412+
this.wrnHeadersSize = 0L;
413+
isWrnLmtReached = false;
414+
}
415+
416+
private ThreadContextStruct(Map<String, String> requestHeaders,
417+
Map<String, List<String>> responseHeaders,
418+
Map<String, Object> transientHeaders, boolean isSystemContext,
419+
long wrnHeadersSize, boolean isWrnLmtReached) {
420+
this.requestHeaders = requestHeaders;
421+
this.responseHeaders = responseHeaders;
422+
this.transientHeaders = transientHeaders;
423+
this.isSystemContext = isSystemContext;
424+
this.wrnHeadersSize = wrnHeadersSize;
425+
this.isWrnLmtReached = isWrnLmtReached;
390426
}
391427

392428
/**
@@ -442,6 +478,19 @@ private ThreadContextStruct putResponseHeaders(Map<String, List<String>> headers
442478

443479
private ThreadContextStruct putResponse(final String key, final String value, final Function<String, String> uniqueValue) {
444480
assert value != null;
481+
long curWrnHeaderSize = 0;
482+
//check if we can add another warning header (max count or size within limits)
483+
if (key.equals("Warning")) {
484+
if (isWrnLmtReached) return this; //can't add warning headers - limit reached
485+
if (maxWrnHeaderCount != -1) { //if count is NOT unbounded, check its limits
486+
int wrnHeaderCount = this.responseHeaders.containsKey("Warning") ? this.responseHeaders.get("Warning").size() : 0;
487+
if (wrnHeaderCount >= maxWrnHeaderCount) return addWrnLmtReachedHeader();
488+
}
489+
if (maxWrnHeaderSize != -1) { //if size is NOT unbounded, check its limits
490+
curWrnHeaderSize = "Warning".getBytes(StandardCharsets.UTF_8).length + value.getBytes(StandardCharsets.UTF_8).length;
491+
if ((wrnHeadersSize + curWrnHeaderSize) > maxWrnHeaderSize) return addWrnLmtReachedHeader();
492+
}
493+
}
445494

446495
final Map<String, List<String>> newResponseHeaders = new HashMap<>(this.responseHeaders);
447496
final List<String> existingValues = newResponseHeaders.get(key);
@@ -460,8 +509,37 @@ private ThreadContextStruct putResponse(final String key, final String value, fi
460509
} else {
461510
newResponseHeaders.put(key, Collections.singletonList(value));
462511
}
512+
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders,
513+
isSystemContext, wrnHeadersSize + curWrnHeaderSize, isWrnLmtReached);
514+
}
463515

464-
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext);
516+
//replace last warning header(s) with "headers limit reached" warning
517+
//respecting limitations on headers size if it is set by user
518+
private ThreadContextStruct addWrnLmtReachedHeader(){
519+
if ((maxWrnHeaderSize == 0) || (maxWrnHeaderCount ==0)) //can't even add "headers limit reached" warning
520+
return new ThreadContextStruct(requestHeaders, responseHeaders, transientHeaders,
521+
isSystemContext, wrnHeadersSize, true);
522+
final Map<String, List<String>> newResponseHeaders = new HashMap<>(this.responseHeaders);
523+
final List<String> wrns = new ArrayList<>(newResponseHeaders.get("Warning"));
524+
final String lastWrnMessage = DeprecationLogger.formatWarning(
525+
"There were more warnings, but they were dropped as [" +
526+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT.getKey() + "] or [" +
527+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE.getKey() + "] were reached!");
528+
529+
if (maxWrnHeaderSize > 0) {
530+
final long wrnSize = "Warning".getBytes(StandardCharsets.UTF_8).length;
531+
wrnHeadersSize = wrnHeadersSize + wrnSize + lastWrnMessage.getBytes(StandardCharsets.UTF_8).length;
532+
do {
533+
String wrn = wrns.remove(wrns.size() - 1);
534+
wrnHeadersSize = wrnHeadersSize - wrnSize - wrn.getBytes(StandardCharsets.UTF_8).length;
535+
} while(wrnHeadersSize > maxWrnHeaderSize);
536+
} else { //we don't care about size as it is unbounded
537+
wrns.remove(wrns.size() - 1);
538+
}
539+
wrns.add(lastWrnMessage);
540+
newResponseHeaders.put("Warning", Collections.unmodifiableList(wrns));
541+
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders,
542+
isSystemContext, wrnHeadersSize, true);
465543
}
466544

467545
private ThreadContextStruct putTransient(String key, Object value) {

server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.elasticsearch.common.unit.TimeValue;
3030

3131
import java.util.List;
32-
import java.util.concurrent.TimeUnit;
3332
import java.util.function.Function;
3433

3534
import static java.util.Collections.emptyList;
@@ -88,6 +87,10 @@ public final class HttpTransportSettings {
8887
Setting.byteSizeSetting("http.max_chunk_size", new ByteSizeValue(8, ByteSizeUnit.KB), Property.NodeScope);
8988
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_HEADER_SIZE =
9089
Setting.byteSizeSetting("http.max_header_size", new ByteSizeValue(8, ByteSizeUnit.KB), Property.NodeScope);
90+
public static final Setting<Integer> SETTING_HTTP_MAX_WARNING_HEADER_COUNT =
91+
Setting.intSetting("http.max_warning_header_count", -1, -1, Setting.Property.Dynamic, Property.NodeScope);
92+
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_WARNING_HEADER_SIZE =
93+
Setting.byteSizeSetting("http.max_warning_header_size", new ByteSizeValue(-1), Setting.Property.Dynamic, Property.NodeScope);
9194
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_INITIAL_LINE_LENGTH =
9295
Setting.byteSizeSetting("http.max_initial_line_length", new ByteSizeValue(4, ByteSizeUnit.KB), Property.NodeScope);
9396
// don't reset cookies by default, since I don't think we really need to

server/src/main/java/org/elasticsearch/node/Node.java

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.node;
2121

2222
import org.apache.logging.log4j.Logger;
23+
import org.apache.logging.log4j.ThreadContext;
2324
import org.apache.lucene.util.Constants;
2425
import org.apache.lucene.util.IOUtils;
2526
import org.apache.lucene.util.SetOnce;
@@ -93,6 +94,7 @@
9394
import org.elasticsearch.gateway.GatewayService;
9495
import org.elasticsearch.gateway.MetaStateService;
9596
import org.elasticsearch.http.HttpServerTransport;
97+
import org.elasticsearch.http.HttpTransportSettings;
9698
import org.elasticsearch.index.analysis.AnalysisRegistry;
9799
import org.elasticsearch.indices.IndicesModule;
98100
import org.elasticsearch.indices.IndicesService;
@@ -351,6 +353,12 @@ protected Node(final Environment environment, Collection<Class<? extends Plugin>
351353
listener::onNewInfo);
352354
final UsageService usageService = new UsageService(settings);
353355

356+
357+
clusterService.getClusterSettings().addSettingsUpdateConsumer(HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT,
358+
org.elasticsearch.common.util.concurrent.ThreadContext::setMaxWarningHeaderCount);
359+
clusterService.getClusterSettings().addSettingsUpdateConsumer(HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE,
360+
org.elasticsearch.common.util.concurrent.ThreadContext::setMaxWarningHeaderSize);
361+
354362
ModulesBuilder modules = new ModulesBuilder();
355363
// plugin modules must be added here, before others or we can get crazy injection errors...
356364
for (Module pluginModule : pluginsService.createGuiceModules()) {

server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java

+55
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Map;
3434
import java.util.Set;
3535
import java.util.stream.IntStream;
36+
import java.nio.charset.StandardCharsets;
3637

3738
import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
3839
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
@@ -246,6 +247,60 @@ public void testEncode() {
246247
assertThat(DeprecationLogger.encode(s), IsSame.sameInstance(s));
247248
}
248249

250+
251+
public void testWarningHeaderCountSetting() throws IOException{
252+
// Test that the number of warning headers don't exceed 'http.max_warning_header_count'
253+
final int maxWarningHeaderCount = 2;
254+
Settings settings = Settings.builder()
255+
.put("http.max_warning_header_count", maxWarningHeaderCount)
256+
.build();
257+
try (ThreadContext threadContext = new ThreadContext(settings)) {
258+
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
259+
// try to log three warning messages
260+
logger.deprecated(threadContexts, "A simple message 1");
261+
logger.deprecated(threadContexts, "A simple message 2");
262+
logger.deprecated(threadContexts, "A simple message 3");
263+
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
264+
final List<String> responses = responseHeaders.get("Warning");
265+
266+
assertEquals(maxWarningHeaderCount, responses.size());
267+
assertThat(responses.get(0), warningValueMatcher);
268+
assertThat(responses.get(0), containsString("\"A simple message 1"));
269+
assertThat(responses.get(1), warningValueMatcher);
270+
assertThat(responses.get(1), containsString("\"There were more warnings, but they were dropped as "));
271+
}
272+
}
273+
274+
public void testWarningHeaderSizeSetting() throws IOException{
275+
// Test that the size of warning headers don't exceed 'http.max_warning_header_size'
276+
Settings settings = Settings.builder()
277+
.put("http.max_warning_header_size", "1Kb")
278+
.build();
279+
280+
byte [] arr = new byte[300];
281+
String message1 = new String(arr, StandardCharsets.UTF_8) + "1";
282+
String message2 = new String(arr, StandardCharsets.UTF_8) + "2";
283+
String message3 = new String(arr, StandardCharsets.UTF_8) + "3";
284+
285+
try (ThreadContext threadContext = new ThreadContext(settings)) {
286+
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
287+
// try to log three warning messages
288+
logger.deprecated(threadContexts, message1);
289+
logger.deprecated(threadContexts, message2);
290+
logger.deprecated(threadContexts, message3);
291+
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
292+
final List<String> responses = responseHeaders.get("Warning");
293+
294+
long warningHeadersSize = 0L;
295+
for (String response : responses){
296+
warningHeadersSize += "Warning".getBytes(StandardCharsets.UTF_8).length +
297+
response.getBytes(StandardCharsets.UTF_8).length;
298+
}
299+
// assert that the size of all warning headers is less or equal to 1Kb
300+
assertTrue(warningHeadersSize <= 1024);
301+
}
302+
}
303+
249304
private String range(int lowerInclusive, int upperInclusive) {
250305
return IntStream
251306
.range(lowerInclusive, upperInclusive + 1)

0 commit comments

Comments
 (0)