Skip to content

Commit 3f2d8c1

Browse files
authored
Merge branch 'main' into changelogs-08-04
2 parents 7509af9 + 1bc289f commit 3f2d8c1

82 files changed

Lines changed: 2326 additions & 499 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ToStringBenchmark.java

Lines changed: 572 additions & 0 deletions
Large diffs are not rendered by default.

docs/changelog/153838.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
area: Reindex
2+
issues: []
3+
pr: 153838
4+
summary: Charge remote reindex HTTP buffers to request breaker
5+
type: bug

docs/changelog/155377.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
area: "ES|QL"
2+
issues: []
3+
pr: 155377
4+
summary: Speed up conversion of long to string for ES|QL type conversions
5+
type: enhancement
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.reindex.remote;
11+
12+
import org.apache.http.HttpResponse;
13+
import org.apache.http.nio.protocol.HttpAsyncResponseConsumer;
14+
import org.elasticsearch.client.HttpAsyncResponseConsumerFactory;
15+
import org.elasticsearch.common.breaker.CircuitBreaker;
16+
17+
import java.util.Objects;
18+
19+
/**
20+
* Creates one breaker-aware response consumer per remote reindex request attempt.
21+
*/
22+
final class BreakerAwareConsumerFactory implements HttpAsyncResponseConsumerFactory {
23+
24+
private final CircuitBreaker breaker;
25+
26+
BreakerAwareConsumerFactory(CircuitBreaker breaker) {
27+
this.breaker = Objects.requireNonNull(breaker, "breaker");
28+
}
29+
30+
@Override
31+
public HttpAsyncResponseConsumer<HttpResponse> createHttpAsyncResponseConsumer() {
32+
return new BreakerAwareHeapBufferedAsyncResponseConsumer(breaker);
33+
}
34+
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.reindex.remote;
11+
12+
import org.apache.http.ContentTooLongException;
13+
import org.apache.http.HttpEntity;
14+
import org.apache.http.HttpException;
15+
import org.apache.http.HttpResponse;
16+
import org.apache.http.entity.ContentType;
17+
import org.apache.http.entity.HttpEntityWrapper;
18+
import org.apache.http.nio.ContentDecoder;
19+
import org.apache.http.nio.IOControl;
20+
import org.apache.http.nio.entity.ContentBufferEntity;
21+
import org.apache.http.nio.protocol.AbstractAsyncResponseConsumer;
22+
import org.apache.http.nio.util.ByteBufferAllocator;
23+
import org.apache.http.nio.util.ContentInputBuffer;
24+
import org.apache.http.nio.util.SimpleInputBuffer;
25+
import org.apache.http.protocol.HttpContext;
26+
import org.elasticsearch.common.breaker.CircuitBreaker;
27+
import org.elasticsearch.core.Releasable;
28+
29+
import java.io.FilterInputStream;
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.io.OutputStream;
33+
import java.nio.ByteBuffer;
34+
import java.util.Objects;
35+
import java.util.concurrent.atomic.AtomicBoolean;
36+
37+
/// Heap-buffered async response consumer that charges the raw Apache HTTP response buffer to the
38+
/// [CircuitBreaker#REQUEST] circuit breaker.
39+
///
40+
/// The low-level REST client buffers each async response in a [SimpleInputBuffer] before
41+
/// invoking the application callback, so heap can be exhausted before the caller sees any bytes. This
42+
/// consumer accounts the actual [ByteBuffer] allocations used by that Apache buffer, including
43+
/// growth for responses without a `Content-Length` header. Responses are not subject to a fixed size
44+
/// cap, whether the length is known or chunked; they grow under the control of the circuit breaker,
45+
/// which is expected to trip before heap is exhausted, up to a hard `MAX_BUFFER_CAPACITY` ceiling that
46+
/// only guards against overflowing the `int`-indexed buffer.
47+
///
48+
/// On a successful response, Apache calls [#releaseResources()] before the caller reads the
49+
/// returned entity. For that reason the breaker reservation is attached to the response entity and
50+
/// must be released after the entity content has been consumed. Failed or cancelled requests release
51+
/// directly from [#releaseResources()].
52+
final class BreakerAwareHeapBufferedAsyncResponseConsumer extends AbstractAsyncResponseConsumer<HttpResponse> {
53+
54+
static final String REMOTE_RESPONSE_BUFFER_BREAKER_LABEL = "reindex_remote_response_buffer";
55+
private static final int INITIAL_BUFFER_SIZE = 4096;
56+
/**
57+
* Hard upper bound on the capacity we grow an unknown-length (chunked) response buffer to. This is
58+
* the JVM's maximum safe array size, not a policy limit — the REQUEST breaker is expected to trip
59+
* long before this. It exists only so growth fails cleanly with a {@link ContentTooLongException}
60+
* rather than overflowing the {@code int}-indexed buffer.
61+
*/
62+
private static final int MAX_BUFFER_CAPACITY = Integer.MAX_VALUE - 8;
63+
64+
private final AccountingByteBufferAllocator allocator;
65+
66+
private volatile HttpResponse response;
67+
private volatile ContentInputBuffer contentBuffer;
68+
private volatile boolean responseDelivered;
69+
70+
BreakerAwareHeapBufferedAsyncResponseConsumer(CircuitBreaker breaker) {
71+
this.allocator = new AccountingByteBufferAllocator(Objects.requireNonNull(breaker, "breaker"));
72+
}
73+
74+
// Visible for testing
75+
long currentReservation() {
76+
return allocator.currentReservation();
77+
}
78+
79+
@Override
80+
protected void onResponseReceived(HttpResponse httpResponse) throws HttpException, IOException {
81+
this.response = httpResponse;
82+
}
83+
84+
@Override
85+
protected void onEntityEnclosed(HttpEntity entity, ContentType contentType) throws IOException {
86+
long len = entity.getContentLength();
87+
if (len > MAX_BUFFER_CAPACITY) {
88+
throw new ContentTooLongException(
89+
"entity content is too long [" + len + "] for the maximum buffer capacity [" + MAX_BUFFER_CAPACITY + "]"
90+
);
91+
}
92+
int initialBufferSize = len < 0 ? INITIAL_BUFFER_SIZE : Math.toIntExact(len);
93+
contentBuffer = new AccountingSimpleInputBuffer(initialBufferSize, allocator);
94+
this.response.setEntity(new ReleasableContentBufferEntity(new ContentBufferEntity(entity, contentBuffer), allocator));
95+
}
96+
97+
@Override
98+
protected void onContentReceived(ContentDecoder decoder, IOControl ioctrl) throws IOException {
99+
try {
100+
contentBuffer.consumeContent(decoder);
101+
} catch (ContentTooLongRuntimeException e) {
102+
throw e.unwrap();
103+
}
104+
}
105+
106+
@Override
107+
protected HttpResponse buildResult(HttpContext context) {
108+
responseDelivered = true;
109+
return response;
110+
}
111+
112+
@Override
113+
protected void releaseResources() {
114+
// AbstractAsyncResponseConsumer calls releaseResources() after building the
115+
// successful HttpResponse, before the caller has consumed the response entity.
116+
// On that path, ownership of the buffer reservation has moved to the
117+
// ReleasableContentBufferEntity, which releases it when the entity content is
118+
// closed. If no response was delivered because the request failed or was
119+
// cancelled, there is no caller-owned entity, so the consumer must release the
120+
// reservation here.
121+
try {
122+
if (responseDelivered == false) {
123+
allocator.close();
124+
}
125+
} finally {
126+
response = null;
127+
contentBuffer = null;
128+
}
129+
}
130+
131+
private final class AccountingSimpleInputBuffer extends SimpleInputBuffer {
132+
private final AccountingByteBufferAllocator allocator;
133+
134+
private AccountingSimpleInputBuffer(int bufferSize, AccountingByteBufferAllocator allocator) {
135+
super(bufferSize, allocator);
136+
this.allocator = allocator;
137+
}
138+
139+
@Override
140+
protected void expand() {
141+
// Growth is bounded by the REQUEST breaker, charged in allocator.allocate() below, up to a
142+
// hard MAX_BUFFER_CAPACITY ceiling. We override expand() here to ensure the
143+
// previous buffer's reserved bytes against the circuit breaker are released.
144+
int oldCapacity = buffer.capacity();
145+
if (oldCapacity >= MAX_BUFFER_CAPACITY) {
146+
throw new ContentTooLongRuntimeException(
147+
new ContentTooLongException("response buffer exceeded maximum capacity [" + MAX_BUFFER_CAPACITY + "] bytes")
148+
);
149+
}
150+
long doubledCapacity = ((long) oldCapacity + 1) << 1;
151+
int newCapacity = Math.toIntExact(Math.min(doubledCapacity, MAX_BUFFER_CAPACITY));
152+
153+
ByteBuffer oldBuffer = buffer;
154+
ByteBuffer newBuffer = allocator.allocate(newCapacity);
155+
boolean success = false;
156+
try {
157+
oldBuffer.flip();
158+
newBuffer.put(oldBuffer);
159+
success = true;
160+
} finally {
161+
if (success) {
162+
buffer = newBuffer;
163+
allocator.release(oldCapacity);
164+
} else {
165+
allocator.release(newCapacity);
166+
}
167+
}
168+
}
169+
}
170+
171+
// package-private for testing so the allocator's thread-safety contract can be unit-tested directly
172+
static final class AccountingByteBufferAllocator implements ByteBufferAllocator, Releasable {
173+
private final CircuitBreaker breaker;
174+
private final Object mutex = new Object();
175+
private boolean closed; // guarded by mutex
176+
private long reservedBytes; // guarded by mutex
177+
178+
AccountingByteBufferAllocator(CircuitBreaker breaker) {
179+
this.breaker = breaker;
180+
}
181+
182+
@Override
183+
public ByteBuffer allocate(int size) {
184+
if (size < 0) {
185+
throw new IllegalArgumentException("size must be >= 0, was " + size);
186+
}
187+
synchronized (mutex) {
188+
if (closed) {
189+
throw new IllegalStateException("allocator is closed");
190+
}
191+
if (size > 0) {
192+
breaker.addEstimateBytesAndMaybeBreak(size, REMOTE_RESPONSE_BUFFER_BREAKER_LABEL);
193+
reservedBytes += size;
194+
}
195+
try {
196+
return ByteBuffer.allocate(size);
197+
} catch (RuntimeException | Error e) {
198+
if (size > 0) {
199+
reservedBytes -= size;
200+
breaker.addWithoutBreaking(-size);
201+
}
202+
throw e;
203+
}
204+
}
205+
}
206+
207+
void release(long bytes) {
208+
if (bytes > 0) {
209+
synchronized (mutex) {
210+
if (closed == false) {
211+
reservedBytes -= bytes;
212+
breaker.addWithoutBreaking(-bytes);
213+
}
214+
}
215+
}
216+
}
217+
218+
long currentReservation() {
219+
synchronized (mutex) {
220+
return reservedBytes;
221+
}
222+
}
223+
224+
@Override
225+
public void close() {
226+
synchronized (mutex) {
227+
if (closed) {
228+
return;
229+
}
230+
closed = true;
231+
if (reservedBytes > 0) {
232+
breaker.addWithoutBreaking(-reservedBytes);
233+
}
234+
reservedBytes = 0L;
235+
}
236+
}
237+
}
238+
239+
private static final class ReleasableContentBufferEntity extends HttpEntityWrapper implements Releasable {
240+
private final Releasable releasable;
241+
private final AtomicBoolean closed = new AtomicBoolean();
242+
243+
private ReleasableContentBufferEntity(HttpEntity wrappedEntity, Releasable releasable) {
244+
super(wrappedEntity);
245+
this.releasable = releasable;
246+
}
247+
248+
@Override
249+
public InputStream getContent() throws IOException {
250+
return new FilterInputStream(super.getContent()) {
251+
@Override
252+
public void close() throws IOException {
253+
try {
254+
super.close();
255+
} finally {
256+
ReleasableContentBufferEntity.this.close();
257+
}
258+
}
259+
};
260+
}
261+
262+
@Override
263+
public void writeTo(OutputStream outStream) throws IOException {
264+
try {
265+
super.writeTo(outStream);
266+
} finally {
267+
close();
268+
}
269+
}
270+
271+
@Override
272+
public void close() {
273+
if (closed.compareAndSet(false, true)) {
274+
releasable.close();
275+
}
276+
}
277+
}
278+
279+
private static final class ContentTooLongRuntimeException extends RuntimeException {
280+
private final ContentTooLongException contentTooLongException;
281+
282+
private ContentTooLongRuntimeException(ContentTooLongException cause) {
283+
super(cause);
284+
this.contentTooLongException = cause;
285+
}
286+
287+
private ContentTooLongException unwrap() {
288+
return contentTooLongException;
289+
}
290+
}
291+
}

0 commit comments

Comments
 (0)