Skip to content

Commit 3c4d175

Browse files
andselalexcams
andauthored
BufferedTokenizer: size limit check in flush and logging when drop data (#19312)
Updates the BufferedTokenizer flush method to raise an error if the remainder of the token is oversized, respect to the size limit. Adds a warning log when data is dropped in the accumulation phase. Co-authored-by: Álex Cámara Lara <alex.camara@elastic.co>
1 parent f1ad935 commit 3c4d175

3 files changed

Lines changed: 227 additions & 4 deletions

File tree

logstash-core/src/main/java/org/logstash/common/BufferedTokenizer.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import java.util.Iterator;
2222
import java.util.NoSuchElementException;
23+
import org.apache.logging.log4j.LogManager;
24+
import org.apache.logging.log4j.Logger;
2325

2426
public class BufferedTokenizer {
2527

@@ -40,12 +42,15 @@ public boolean hasNext() {
4042
}
4143

4244
static class DataSplitter implements Iterator<String> {
45+
private static final Logger logger = LogManager.getLogger(BufferedTokenizer.class);
46+
4347
private final String separator;
4448
private int currentIdx = 0;
4549
private int nextSeparatorIdx = -1;
4650
private final StringBuilder accumulator = new StringBuilder();
4751
private final int sizeLimit;
4852
private int lastFragmentSize = 0;
53+
private long droppedBytesCount = 0;
4954

5055
DataSplitter(String separator) {
5156
this.separator = separator;
@@ -109,10 +114,17 @@ public void append(String data) {
109114
if (isSizeLimitSet()) {
110115
if (!data.contains(separator) && lastFragmentSize > sizeLimit) {
111116
// stop accumulating if last fragments already reached the sizeLimit
117+
droppedBytesCount += data.length();
112118
return;
113119
}
114120

115-
// we know that data contains at least one separator or that we haven't yet reached the first separator instance, update lastFragmentSize
121+
if (droppedBytesCount > 0) {
122+
logger.warn("Input buffer exceeded the sizeLimit of {} and dropped {} bytes from an oversized token", sizeLimit, droppedBytesCount);
123+
droppedBytesCount = 0;
124+
}
125+
126+
// we know that data contains at least one separator or that we haven't yet reached
127+
// the first separator instance, update lastFragmentSize
116128
int lastSeparatorIdx = data.lastIndexOf(separator);
117129
if (lastSeparatorIdx == -1) {
118130
lastFragmentSize += data.length();
@@ -128,11 +140,27 @@ private boolean isSizeLimitSet() {
128140
}
129141

130142
public String flush() {
143+
if (isSizeLimitSet()) {
144+
if (droppedBytesCount > 0) {
145+
logger.warn("Input buffer exceeded the sizeLimit of {} and dropped {} bytes from an oversized token", sizeLimit, droppedBytesCount);
146+
}
147+
if (lastFragmentSize > sizeLimit) {
148+
cleanAccumulatorState();
149+
throw new IllegalStateException("input buffer full, consumed token which exceeded the sizeLimit " + sizeLimit);
150+
}
151+
}
131152
final String flushed = accumulator.substring(currentIdx);
153+
// empty the accumulator
154+
cleanAccumulatorState();
155+
return flushed;
156+
}
157+
158+
private void cleanAccumulatorState() {
132159
// empty the accumulator
133160
accumulator.setLength(0);
134161
currentIdx = 0;
135-
return flushed;
162+
lastFragmentSize = 0;
163+
droppedBytesCount = 0;
136164
}
137165

138166
// considered empty if caught up to the accumulator
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.logstash.common;
21+
22+
import org.apache.logging.log4j.core.test.appender.ListAppender;
23+
import org.apache.logging.log4j.core.test.junit.LoggerContextRule;
24+
import org.junit.Before;
25+
import org.junit.ClassRule;
26+
import org.junit.Test;
27+
28+
import java.util.List;
29+
30+
import static org.hamcrest.MatcherAssert.assertThat;
31+
import static org.hamcrest.Matchers.containsString;
32+
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.assertThrows;
34+
import static org.junit.Assert.assertTrue;
35+
36+
/**
37+
* Verifies that BufferedTokenizer logs a WARN message with the dropped byte count whenever
38+
* buffered data is discarded due to the sizeLimit being exceeded.
39+
*
40+
* Two trigger points are tested:
41+
* 1. When a separator arrives after a sequence of dropped fragments (recovery in append).
42+
* 2. When flush() is called while dropped data is outstanding.
43+
*/
44+
public final class BufferedTokenizerDroppedDataLoggingTest {
45+
46+
private static final String CONFIG = "log4j2-test1.xml";
47+
48+
@ClassRule
49+
public static LoggerContextRule CTX = new LoggerContextRule(CONFIG);
50+
51+
private ListAppender appender;
52+
private BufferedTokenizer sut;
53+
54+
@Before
55+
public void setUp() {
56+
appender = CTX.getListAppender("EventLogger").clear();
57+
sut = new BufferedTokenizer("\n", 10);
58+
}
59+
60+
@Test
61+
public void givenDroppedFragmentsWhenSeparatorArrivesInAppendThenWarnIsLoggedWithDroppedByteCount() {
62+
// "01234567890" (11 chars) — NOT dropped because lastFragmentSize starts at 0
63+
sut.extract("01234567890");
64+
65+
// "AAAAA" (5 chars, no sep) — dropped: lastFragmentSize(11) > sizeLimit(10)
66+
sut.extract("AAAAA");
67+
68+
// "BBBBB" (5 chars, no sep) — dropped: still 11 > 10
69+
sut.extract("BBBBB");
70+
71+
// No warn yet — separator hasn't arrived
72+
assertTrue("No warning should be emitted before a separator is seen",
73+
warnMessages().stream().noneMatch(m -> m.contains("dropped")));
74+
75+
// "\nCC" contains a separator → recovery triggers the warn with 10 dropped bytes
76+
sut.extract("\nCC");
77+
78+
assertEquals(1, warnMessages().size());
79+
assertThat("Warning must report 10 dropped bytes (AAAAA + BBBBB)", first(warnMessages()), containsString("dropped 10 bytes"));
80+
}
81+
82+
@Test
83+
public void givenMultipleBatchesOfDroppedDataWhenSeparatorArrivesRepeatedly_ThenEachBatchIsLoggedSeparately() {
84+
// First overrun: 11 chars, no sep → accumulated (lastFragmentSize = 11)
85+
sut.extract("01234567890");
86+
87+
// Drop 3 bytes
88+
sut.extract("AAA");
89+
// Recovery: separator seen → warn("3 bytes dropped")
90+
sut.extract("\n");
91+
92+
assertEquals(1, warnMessages().size());
93+
assertThat("First warn should report 3 dropped bytes", first(warnMessages()), containsString("dropped 3 bytes"));
94+
95+
// Start a new overrun: 11 chars again → accumulated on top of existing content
96+
sut.extract("01234567890");
97+
// Drop 7 bytes
98+
sut.extract("BBBBBBB");
99+
// Recovery: separator seen → warn("7 bytes dropped")
100+
sut.extract("\n");
101+
102+
assertEquals(2, warnMessages().size());
103+
assertThat("Second warn should report 7 dropped bytes", last(warnMessages()), containsString("dropped 7 bytes"));
104+
}
105+
106+
@Test
107+
public void givenDroppedFragmentsWhenFlushIsInvokedThenWarnIsLoggedWithDroppedByteCountBeforeThrowing() {
108+
// "01234567890" (11 chars) — accumulated, lastFragmentSize = 11
109+
sut.extract("01234567890");
110+
111+
// Drop 4 bytes
112+
sut.extract("CCCC");
113+
// Drop 6 bytes
114+
sut.extract("DDDDDD");
115+
116+
// No warn yet — separator hasn't arrived and flush not called
117+
assertTrue("No warning before flush", warnMessages().stream().noneMatch(m -> m.contains("dropped")));
118+
119+
// flush() must warn about dropped data then throw for the overrun partial token
120+
assertThrows(IllegalStateException.class, () -> sut.flush());
121+
122+
assertEquals(1, warnMessages().size());
123+
assertThat("Warning must report 10 dropped bytes (CCCC + DDDDDD)", first(warnMessages()), containsString("dropped 10 bytes"));
124+
}
125+
126+
@Test
127+
public void givenDroppedFragmentsWhenFlushIsInvokedThenWarnPrecedesTheException() {
128+
sut.extract("01234567890"); // accumulated, lastFragmentSize = 11
129+
sut.extract("EEE"); // dropped (3 bytes)
130+
131+
assertThrows(IllegalStateException.class, () -> sut.flush());
132+
133+
// The warn must have been emitted (before the exception propagated)
134+
assertEquals(1, warnMessages().size());
135+
assertThat("Dropped-data warn must be logged even when flush throws", first(warnMessages()), containsString("dropped 3 bytes"));
136+
}
137+
138+
@Test
139+
public void givenNoDroppedDataWhenFlushIsInvokedThenNoWarnIsLogged() {
140+
sut.extract("short");
141+
sut.flush();
142+
143+
assertTrue("No dropped-data warn should appear when nothing was dropped",
144+
warnMessages().stream().noneMatch(m -> m.contains("dropped")));
145+
}
146+
147+
@Test
148+
public void givenNoDroppedDataWhenSeparatorArrivesNoWarnIsLogged() {
149+
sut.extract("hello\nworld");
150+
151+
assertTrue("No dropped-data warn should appear for normal tokenization",
152+
warnMessages().stream().noneMatch(m -> m.contains("dropped")));
153+
}
154+
155+
private List<String> warnMessages() {
156+
return appender.getMessages();
157+
}
158+
159+
private static String first(List<String> list) {
160+
return list.get(0);
161+
}
162+
163+
private static String last(List<String> list) {
164+
return list.get(list.size() - 1);
165+
}
166+
}

logstash-core/src/test/java/org/logstash/common/BufferedTokenizerWithSizeLimitTest.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525

2626
import java.util.Iterator;
2727
import java.util.List;
28+
import java.util.NoSuchElementException;
2829

2930
import static org.hamcrest.MatcherAssert.assertThat;
3031
import static org.hamcrest.Matchers.containsString;
32+
import static org.hamcrest.Matchers.emptyOrNullString;
3133
import static org.hamcrest.Matchers.is;
3234
import static org.hamcrest.Matchers.lessThan;
3335
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertFalse;
3437
import static org.junit.Assert.assertThrows;
3538
import static org.junit.Assert.assertTrue;
3639
import static org.logstash.common.BufferedTokenizerTest.toList;
@@ -47,7 +50,32 @@ public void setUp() {
4750
private void initSUTWithSizeLimit(int sizeLimit) {
4851
sut = new BufferedTokenizer("\n", sizeLimit);
4952
}
50-
53+
54+
@Test
55+
public void givenOversizedFragmentWithoutSeparatorWhenFlushIsInvokedThenThrows() {
56+
// Provide an overrun fragment without delimiter, from this point on
57+
// the BufferedTokenizer start dropping data because already passed the
58+
// sizeLimit.
59+
Iterable<String> it = sut.extract("01234567890");
60+
Iterator<String> ite = it.iterator();
61+
verifyNoTokensAvailableOnReadSide(ite);
62+
63+
// Provide another fragment which is inside the sizeLimit and DO NOT contain a delimiter.
64+
// Reuse the previous iterator, it's the same returned by this call.
65+
sut.extract("AAAAA");
66+
verifyNoTokensAvailableOnReadSide(ite);
67+
68+
// Exercise flush and expect it throws an exception for the overrun partial token
69+
Exception thrownException = assertThrows(IllegalStateException.class, () -> sut.flush());
70+
assertThat(thrownException.getMessage(), containsString("input buffer full"));
71+
}
72+
73+
private void verifyNoTokensAvailableOnReadSide(Iterator<String> ite) {
74+
assertFalse(ite.hasNext());
75+
Exception thrownException = assertThrows(NoSuchElementException.class, ite::next);
76+
assertThat(thrownException.getMessage(), is(emptyOrNullString()));
77+
}
78+
5179
@Test
5280
public void givenTokenWithinSizeLimitWhenExtractedThenReturnTokens() {
5381
List<String> tokens = toList(sut.extract("foo\nbar\n"));
@@ -146,7 +174,8 @@ public void givenSequenceOfFragmentsWithoutSeparatorThenDoesntGenerateOutOfMemor
146174

147175
// with the second fragment passed to extract it overrun the sizeLimit, the tokenizer
148176
// drop starting from the third fragment
149-
assertThat("Accumulator include only a part of an exploding payload", sut.flush().length(), is(lessThan(neverEndingData.length() * 3)));
177+
Exception thrownException = assertThrows(IllegalStateException.class, () -> sut.flush());
178+
assertThat(thrownException.getMessage(), containsString("input buffer full"));
150179

151180
Iterable<String> tokensIterable = sut.extract("\nbbb\n");
152181
Iterator<String> tokensIterator = tokensIterable.iterator();

0 commit comments

Comments
 (0)