Skip to content

Commit 5a4010c

Browse files
authored
Enforcing max size for random samples (#136134)
1 parent ea2fb07 commit 5a4010c

File tree

5 files changed

+125
-12
lines changed

5 files changed

+125
-12
lines changed

server/src/main/java/org/elasticsearch/ingest/SamplingService.java

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
2727
import org.elasticsearch.common.xcontent.XContentHelper;
2828
import org.elasticsearch.core.TimeValue;
29+
import org.elasticsearch.core.Tuple;
2930
import org.elasticsearch.logging.LogManager;
3031
import org.elasticsearch.logging.Logger;
3132
import org.elasticsearch.script.IngestConditionalScript;
@@ -147,10 +148,14 @@ private void maybeSample(
147148
SampleStats stats = sampleInfo.stats;
148149
stats.potentialSamples.increment();
149150
try {
150-
if (sampleInfo.hasCapacity() == false) {
151+
if (sampleInfo.isFull) {
151152
stats.samplesRejectedForMaxSamplesExceeded.increment();
152153
return;
153154
}
155+
if (sampleInfo.getSizeInBytes() + indexRequest.source().length() > samplingConfig.maxSize().getBytes()) {
156+
stats.samplesRejectedForSize.increment();
157+
return;
158+
}
154159
if (Math.random() >= samplingConfig.rate()) {
155160
stats.samplesRejectedForRate.increment();
156161
return;
@@ -306,6 +311,10 @@ public void writeTo(StreamOutput out) throws IOException {
306311
XContentHelper.writeTo(out, contentType);
307312
}
308313

314+
public long getSizeInBytes() {
315+
return indexName.length() + source.length;
316+
}
317+
309318
@Override
310319
public boolean equals(Object o) {
311320
if (this == o) return true;
@@ -346,6 +355,7 @@ public static final class SampleStats implements Writeable, ToXContent {
346355
final LongAdder samplesRejectedForCondition = new LongAdder();
347356
final LongAdder samplesRejectedForRate = new LongAdder();
348357
final LongAdder samplesRejectedForException = new LongAdder();
358+
final LongAdder samplesRejectedForSize = new LongAdder();
349359
final LongAdder timeSamplingInNanos = new LongAdder();
350360
final LongAdder timeEvaluatingConditionInNanos = new LongAdder();
351361
final LongAdder timeCompilingConditionInNanos = new LongAdder();
@@ -367,6 +377,7 @@ public SampleStats(
367377
long samplesRejectedForCondition,
368378
long samplesRejectedForRate,
369379
long samplesRejectedForException,
380+
long samplesRejectedForSize,
370381
TimeValue timeSampling,
371382
TimeValue timeEvaluatingCondition,
372383
TimeValue timeCompilingCondition,
@@ -378,6 +389,7 @@ public SampleStats(
378389
this.samplesRejectedForCondition.add(samplesRejectedForCondition);
379390
this.samplesRejectedForRate.add(samplesRejectedForRate);
380391
this.samplesRejectedForException.add(samplesRejectedForException);
392+
this.samplesRejectedForSize.add(samplesRejectedForSize);
381393
this.timeSamplingInNanos.add(timeSampling.nanos());
382394
this.timeEvaluatingConditionInNanos.add(timeEvaluatingCondition.nanos());
383395
this.timeCompilingConditionInNanos.add(timeCompilingCondition.nanos());
@@ -390,6 +402,7 @@ public SampleStats(StreamInput in) throws IOException {
390402
samplesRejectedForCondition.add(in.readLong());
391403
samplesRejectedForRate.add(in.readLong());
392404
samplesRejectedForException.add(in.readLong());
405+
samplesRejectedForSize.add(in.readLong());
393406
samples.add(in.readLong());
394407
timeSamplingInNanos.add(in.readLong());
395408
timeEvaluatingConditionInNanos.add(in.readLong());
@@ -425,6 +438,10 @@ public long getSamplesRejectedForException() {
425438
return samplesRejectedForException.longValue();
426439
}
427440

441+
public long getSamplesRejectedForSize() {
442+
return samplesRejectedForSize.longValue();
443+
}
444+
428445
public TimeValue getTimeSampling() {
429446
return TimeValue.timeValueNanos(timeSamplingInNanos.longValue());
430447
}
@@ -475,6 +492,7 @@ private static void addAllFields(SampleStats source, SampleStats dest) {
475492
dest.samplesRejectedForCondition.add(source.samplesRejectedForCondition.longValue());
476493
dest.samplesRejectedForRate.add(source.samplesRejectedForRate.longValue());
477494
dest.samplesRejectedForException.add(source.samplesRejectedForException.longValue());
495+
dest.samplesRejectedForSize.add(source.samplesRejectedForSize.longValue());
478496
dest.samples.add(source.samples.longValue());
479497
dest.timeSamplingInNanos.add(source.timeSamplingInNanos.longValue());
480498
dest.timeEvaluatingConditionInNanos.add(source.timeEvaluatingConditionInNanos.longValue());
@@ -492,6 +510,7 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par
492510
builder.field("samples_rejected_for_condition", samplesRejectedForCondition.longValue());
493511
builder.field("samples_rejected_for_rate", samplesRejectedForRate.longValue());
494512
builder.field("samples_rejected_for_exception", samplesRejectedForException.longValue());
513+
builder.field("samples_rejected_for_size", samplesRejectedForSize.longValue());
495514
builder.field("samples_accepted", samples.longValue());
496515
builder.humanReadableField("time_sampling_millis", "time_sampling", TimeValue.timeValueNanos(timeSamplingInNanos.longValue()));
497516
builder.humanReadableField(
@@ -515,6 +534,7 @@ public void writeTo(StreamOutput out) throws IOException {
515534
out.writeLong(samplesRejectedForCondition.longValue());
516535
out.writeLong(samplesRejectedForRate.longValue());
517536
out.writeLong(samplesRejectedForException.longValue());
537+
out.writeLong(samplesRejectedForSize.longValue());
518538
out.writeLong(samples.longValue());
519539
out.writeLong(timeSamplingInNanos.longValue());
520540
out.writeLong(timeEvaluatingConditionInNanos.longValue());
@@ -558,6 +578,9 @@ public boolean equals(Object o) {
558578
if (samplesRejectedForException.longValue() != that.samplesRejectedForException.longValue()) {
559579
return false;
560580
}
581+
if (samplesRejectedForSize.longValue() != that.samplesRejectedForSize.longValue()) {
582+
return false;
583+
}
561584
if (timeSamplingInNanos.longValue() != that.timeSamplingInNanos.longValue()) {
562585
return false;
563586
}
@@ -598,6 +621,7 @@ public int hashCode() {
598621
samplesRejectedForCondition.longValue(),
599622
samplesRejectedForRate.longValue(),
600623
samplesRejectedForException.longValue(),
624+
samplesRejectedForSize.longValue(),
601625
timeSamplingInNanos.longValue(),
602626
timeEvaluatingConditionInNanos.longValue(),
603627
timeCompilingConditionInNanos.longValue()
@@ -637,14 +661,23 @@ public SampleStats adjustForMaxSize(int maxSize) {
637661
*/
638662
private static final class SampleInfo {
639663
private final RawDocument[] rawDocuments;
664+
/*
665+
* This stores the maximum index in rawDocuments that has data currently. This is incremented speculatively before writing data to
666+
* the array, so it is possible that this index is rawDocuments.length or greater.
667+
*/
668+
private final AtomicInteger rawDocumentsIndex = new AtomicInteger(-1);
669+
/*
670+
* This caches the size of all raw documents in the rawDocuments array up to and including the data at the index on the left side
671+
* of the tuple. The size in bytes is the right side of the tuple.
672+
*/
673+
private volatile Tuple<Integer, Long> sizeInBytesAtIndex = Tuple.tuple(-1, 0L);
640674
private final SampleStats stats;
641675
private final long expiration;
642676
private final TimeValue timeToLive;
643677
private volatile Script script;
644678
private volatile IngestConditionalScript.Factory factory;
645679
private volatile boolean compilationFailed = false;
646680
private volatile boolean isFull = false;
647-
private final AtomicInteger arrayIndex = new AtomicInteger(0);
648681

649682
SampleInfo(int maxSamples, TimeValue timeToLive, long relativeNowMillis) {
650683
this.timeToLive = timeToLive;
@@ -653,10 +686,6 @@ private static final class SampleInfo {
653686
this.expiration = (timeToLive == null ? TimeValue.timeValueDays(5).millis() : timeToLive.millis()) + relativeNowMillis;
654687
}
655688

656-
public boolean hasCapacity() {
657-
return isFull == false;
658-
}
659-
660689
/*
661690
* This returns the array of raw documents. It's size will be the maximum number of raw documents allowed in this sample. Some (or
662691
* all) elements could be null.
@@ -665,11 +694,55 @@ public RawDocument[] getRawDocuments() {
665694
return rawDocuments;
666695
}
667696

697+
/*
698+
* This gets an approximate size in bytes for this sample. It only takes the size of the raw documents into account, since that is
699+
* the only part of the sample that is not a fixed size. This method favors speed over 100% correctness -- it is possible during
700+
* heavy concurrent ingestion that it under-reports the current size.
701+
*/
702+
public long getSizeInBytes() {
703+
/*
704+
* This method could get called very frequently during ingestion. Looping through every RawDocument every time would get
705+
* expensive. Since the data in the rawDocuments array is immutable once it has been written, we store the index and value of
706+
* the computed size if all raw documents up to that index are non-null (i.e. no documents were still in flight as we were
707+
* counting). That way we don't have to re-compute the size for documents we've already looked at.
708+
*/
709+
Tuple<Integer, Long> knownIndexAndSize = sizeInBytesAtIndex;
710+
int knownSizeIndex = knownIndexAndSize.v1();
711+
long knownSize = knownIndexAndSize.v2();
712+
// It is possible that rawDocumentsIndex is beyond the end of rawDocuments
713+
int currentRawDocumentsIndex = Math.min(rawDocumentsIndex.get(), rawDocuments.length - 1);
714+
if (currentRawDocumentsIndex == knownSizeIndex) {
715+
return knownSize;
716+
}
717+
long size = knownSize;
718+
boolean anyNulls = false;
719+
for (int i = knownSizeIndex + 1; i <= currentRawDocumentsIndex; i++) {
720+
RawDocument rawDocument = rawDocuments[i];
721+
if (rawDocument == null) {
722+
/*
723+
* Some documents were in flight and haven't been stored in the array yet, so we'll move past this. The size will be a
724+
* little low on this method call. So we're going to set this flag so that we don't store this value for future use.
725+
*/
726+
anyNulls = true;
727+
} else {
728+
size += rawDocuments[i].getSizeInBytes();
729+
}
730+
}
731+
/*
732+
* The most important thing is for this method to be fast. It is OK if we store the same value twice, or even if we store a
733+
* slightly out-of-date copy, as long as we don't do any locking. The correct size will be calculated next time.
734+
*/
735+
if (anyNulls == false) {
736+
sizeInBytesAtIndex = Tuple.tuple(currentRawDocumentsIndex, size);
737+
}
738+
return size;
739+
}
740+
668741
/*
669742
* Adds the rawDocument to the sample if there is capacity. Returns true if it adds it, or false if it does not.
670743
*/
671744
public boolean offer(RawDocument rawDocument) {
672-
int index = arrayIndex.getAndIncrement();
745+
int index = rawDocumentsIndex.incrementAndGet();
673746
if (index < rawDocuments.length) {
674747
rawDocuments[index] = rawDocument;
675748
if (index == rawDocuments.length - 1) {

server/src/test/java/org/elasticsearch/action/admin/indices/sampling/GetSampleStatsActionNodeResponseTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ private static SamplingService.SampleStats randomStats() {
5252
randomNonNegativeLong(),
5353
randomNonNegativeLong(),
5454
randomNonNegativeLong(),
55+
randomNonNegativeLong(),
5556
randomPositiveTimeValue(),
5657
randomPositiveTimeValue(),
5758
randomPositiveTimeValue(),

server/src/test/java/org/elasticsearch/action/admin/indices/sampling/GetSampleStatsActionResponseTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ private static SamplingService.SampleStats randomStats() {
6767
randomNonNegativeLong(),
6868
randomNonNegativeLong(),
6969
randomNonNegativeLong(),
70+
randomNonNegativeLong(),
7071
randomPositiveTimeValue(),
7172
randomPositiveTimeValue(),
7273
randomPositiveTimeValue(),

server/src/test/java/org/elasticsearch/ingest/SamplingServiceSampleStatsTests.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ protected SampleStats createTestInstance() {
3636
stats.samplesRejectedForCondition.add(randomReasonableLong());
3737
stats.samplesRejectedForRate.add(randomReasonableLong());
3838
stats.samplesRejectedForException.add(randomReasonableLong());
39+
stats.samplesRejectedForSize.add(randomReasonableLong());
3940
stats.timeSamplingInNanos.add(randomReasonableLong());
4041
stats.timeEvaluatingConditionInNanos.add(randomReasonableLong());
4142
stats.timeCompilingConditionInNanos.add(randomReasonableLong());
@@ -58,17 +59,18 @@ private long randomReasonableLong() {
5859
@Override
5960
protected SampleStats mutateInstance(SampleStats instance) throws IOException {
6061
SampleStats mutated = instance.combine(new SampleStats());
61-
switch (between(0, 9)) {
62+
switch (between(0, 10)) {
6263
case 0 -> mutated.samples.add(1);
6364
case 1 -> mutated.potentialSamples.add(1);
6465
case 2 -> mutated.samplesRejectedForMaxSamplesExceeded.add(1);
6566
case 3 -> mutated.samplesRejectedForCondition.add(1);
6667
case 4 -> mutated.samplesRejectedForRate.add(1);
6768
case 5 -> mutated.samplesRejectedForException.add(1);
68-
case 6 -> mutated.timeSamplingInNanos.add(1);
69-
case 7 -> mutated.timeEvaluatingConditionInNanos.add(1);
70-
case 8 -> mutated.timeCompilingConditionInNanos.add(1);
71-
case 9 -> mutated.lastException = mutated.lastException == null
69+
case 6 -> mutated.samplesRejectedForSize.add(1);
70+
case 7 -> mutated.timeSamplingInNanos.add(1);
71+
case 8 -> mutated.timeEvaluatingConditionInNanos.add(1);
72+
case 9 -> mutated.timeCompilingConditionInNanos.add(1);
73+
case 10 -> mutated.lastException = mutated.lastException == null
7274
? new ElasticsearchException(randomAlphanumericOfLength(10))
7375
: null;
7476
default -> throw new IllegalArgumentException("Should never get here");
@@ -104,6 +106,10 @@ public void testCombine() {
104106
stats1CombineStats2.getSamplesRejectedForException(),
105107
equalTo(stats1.getSamplesRejectedForException() + stats2.getSamplesRejectedForException())
106108
);
109+
assertThat(
110+
stats1CombineStats2.getSamplesRejectedForSize(),
111+
equalTo(stats1.getSamplesRejectedForSize() + stats2.getSamplesRejectedForSize())
112+
);
107113
assertThat(
108114
stats1CombineStats2.getTimeSampling(),
109115
equalTo(TimeValue.timeValueNanos(stats1.getTimeSampling().nanos() + stats2.getTimeSampling().nanos()))

server/src/test/java/org/elasticsearch/ingest/SamplingServiceTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.elasticsearch.script.ScriptService;
3030
import org.elasticsearch.test.ClusterServiceUtils;
3131
import org.elasticsearch.test.ESTestCase;
32+
import org.elasticsearch.xcontent.XContentType;
3233

3334
import java.util.HashMap;
3435
import java.util.List;
@@ -96,6 +97,7 @@ public void testMaybeSample() {
9697
assertThat(stats.getSamplesRejectedForRate(), equalTo(0L));
9798
assertThat(stats.getSamplesRejectedForCondition(), equalTo(0L));
9899
assertThat(stats.getSamplesRejectedForException(), equalTo(0L));
100+
assertThat(stats.getSamplesRejectedForSize(), equalTo(0L));
99101
assertThat(stats.getSamplesRejectedForMaxSamplesExceeded(), equalTo(0L));
100102
assertThat(stats.getLastException(), nullValue());
101103
assertThat(stats.getTimeSampling(), greaterThan(TimeValue.ZERO));
@@ -224,6 +226,36 @@ public void testMaybeSampleMaxSamples() {
224226
assertThat(stats.getTimeEvaluatingCondition(), equalTo(TimeValue.ZERO));
225227
}
226228

229+
public void testMaybeSampleMaxSize() {
230+
/*
231+
* This tests that the max size limit on the SamplingConfiguration is enforced. Here we set maxSize to 400. The source field of
232+
* each index request is an array of 150 bytes. Since the size of the raw document is approximately the size of the source byte
233+
* array, we expect to be able to insert 2 raw documents before all others are rejected due to the max size limit.
234+
*/
235+
assumeTrue("Requires sampling feature flag", RANDOM_SAMPLING_FEATURE_FLAG);
236+
SamplingService samplingService = getTestSamplingService();
237+
String indexName = randomIdentifier();
238+
int maxSamples = randomIntBetween(2, 50);
239+
ProjectMetadata.Builder projectBuilder = ProjectMetadata.builder(ProjectId.DEFAULT)
240+
.putCustom(
241+
SamplingMetadata.TYPE,
242+
new SamplingMetadata(
243+
Map.of(
244+
indexName,
245+
new SamplingConfiguration(1.0, maxSamples, ByteSizeValue.ofBytes(400), TimeValue.timeValueDays(3), null)
246+
)
247+
)
248+
);
249+
final ProjectId projectId = projectBuilder.getId();
250+
ProjectMetadata projectMetadata = projectBuilder.build();
251+
final IndexRequest indexRequest = new IndexRequest(indexName).id("_id").source(randomByteArrayOfLength(150), XContentType.JSON);
252+
for (int i = 0; i < maxSamples; i++) {
253+
samplingService.maybeSample(projectMetadata, indexRequest);
254+
}
255+
assertThat(samplingService.getLocalSample(projectId, indexName).size(), equalTo(2));
256+
assertThat(samplingService.getLocalSampleStats(projectId, indexName).getSamplesRejectedForSize(), equalTo((long) maxSamples - 2));
257+
}
258+
227259
private SamplingService getTestSamplingService() {
228260
final ScriptService scriptService = new ScriptService(
229261
Settings.EMPTY,

0 commit comments

Comments
 (0)