Skip to content

Commit 495b543

Browse files
Improve Stability of GCS Mock API (#49592) (#49597)
Same as #49518 pretty much but for GCS. Fixing a few more spots where input stream can get closed without being fully drained and adding assertions to make sure it's always drained. Moved the no-close stream wrapper to production code utilities since there's a number of spots in production code where it's also useful (will reuse it there in a follow-up).
1 parent 26a8ca0 commit 495b543

File tree

5 files changed

+58
-20
lines changed

5 files changed

+58
-20
lines changed

plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ protected String requestUniqueId(HttpExchange exchange) {
213213
if ("/token".equals(exchange.getRequestURI().getPath())) {
214214
try {
215215
// token content is unique per node (not per request)
216-
return Streams.readFully(exchange.getRequestBody()).utf8ToString();
216+
return Streams.readFully(Streams.noCloseStream(exchange.getRequestBody())).utf8ToString();
217217
} catch (IOException e) {
218218
throw new AssertionError("Unable to read token request body", e);
219219
}

server/src/main/java/org/elasticsearch/common/io/Streams.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.common.io.stream.StreamOutput;
2626

2727
import java.io.BufferedReader;
28+
import java.io.FilterInputStream;
2829
import java.io.IOException;
2930
import java.io.InputStream;
3031
import java.io.InputStreamReader;
@@ -219,6 +220,21 @@ public static void readAllLines(InputStream input, Consumer<String> consumer) th
219220
}
220221
}
221222

223+
/**
224+
* Wraps an {@link InputStream} such that it's {@code close} method becomes a noop
225+
*
226+
* @param stream {@code InputStream} to wrap
227+
* @return wrapped {@code InputStream}
228+
*/
229+
public static InputStream noCloseStream(InputStream stream) {
230+
return new FilterInputStream(stream) {
231+
@Override
232+
public void close() {
233+
// noop
234+
}
235+
};
236+
}
237+
222238
/**
223239
* Wraps the given {@link BytesStream} in a {@link StreamOutput} that simply flushes when
224240
* close is called.

test/fixtures/gcs-fixture/src/main/java/fixture/gcs/FakeOAuth2HttpHandler.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@
3030
@SuppressForbidden(reason = "Uses a HttpServer to emulate a fake OAuth2 authentication service")
3131
public class FakeOAuth2HttpHandler implements HttpHandler {
3232

33+
private static final byte[] BUFFER = new byte[1024];
34+
3335
@Override
3436
public void handle(final HttpExchange exchange) throws IOException {
35-
byte[] response = ("{\"access_token\":\"foo\",\"token_type\":\"Bearer\",\"expires_in\":3600}").getBytes(UTF_8);
36-
exchange.getResponseHeaders().add("Content-Type", "application/json");
37-
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
38-
exchange.getResponseBody().write(response);
39-
exchange.close();
37+
try {
38+
byte[] response = ("{\"access_token\":\"foo\",\"token_type\":\"Bearer\",\"expires_in\":3600}").getBytes(UTF_8);
39+
exchange.getResponseHeaders().add("Content-Type", "application/json");
40+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
41+
exchange.getResponseBody().write(response);
42+
while (exchange.getRequestBody().read(BUFFER) >= 0) ;
43+
} finally {
44+
int read = exchange.getRequestBody().read();
45+
assert read == -1 : "Request body should have been fully read here but saw [" + read + "]";
46+
exchange.close();
47+
}
4048
}
4149
}

test/fixtures/gcs-fixture/src/main/java/fixture/gcs/GoogleCloudStorageHttpHandler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public void handle(final HttpExchange exchange) throws IOException {
8181
assert read == -1 : "Request body should have been empty but saw [" + read + "]";
8282
}
8383
try {
84+
// Request body is closed in the finally block
85+
final InputStream wrappedRequest = Streams.noCloseStream(exchange.getRequestBody());
8486
if (Regex.simpleMatch("GET /storage/v1/b/" + bucket + "/o*", request)) {
8587
// List Objects https://cloud.google.com/storage/docs/json_api/v1/objects/list
8688
final Map<String, String> params = new HashMap<>();
@@ -159,7 +161,7 @@ public void handle(final HttpExchange exchange) throws IOException {
159161
// Batch https://cloud.google.com/storage/docs/json_api/v1/how-tos/batch
160162
final String uri = "/storage/v1/b/" + bucket + "/o/";
161163
final StringBuilder batch = new StringBuilder();
162-
for (String line : Streams.readAllLines(new BufferedInputStream(exchange.getRequestBody()))) {
164+
for (String line : Streams.readAllLines(new BufferedInputStream(wrappedRequest))) {
163165
if (line.length() == 0 || line.startsWith("--") || line.toLowerCase(Locale.ROOT).startsWith("content")) {
164166
batch.append(line).append('\n');
165167
} else if (line.startsWith("DELETE")) {
@@ -180,7 +182,7 @@ public void handle(final HttpExchange exchange) throws IOException {
180182

181183
} else if (Regex.simpleMatch("POST /upload/storage/v1/b/" + bucket + "/*uploadType=multipart*", request)) {
182184
// Multipart upload
183-
Optional<Tuple<String, BytesArray>> content = parseMultipartRequestBody(exchange.getRequestBody());
185+
Optional<Tuple<String, BytesArray>> content = parseMultipartRequestBody(wrappedRequest);
184186
if (content.isPresent()) {
185187
blobs.put(content.get().v1(), content.get().v2());
186188

@@ -199,7 +201,7 @@ public void handle(final HttpExchange exchange) throws IOException {
199201
final String blobName = params.get("name");
200202
blobs.put(blobName, BytesArray.EMPTY);
201203

202-
byte[] response = Streams.readFully(exchange.getRequestBody()).utf8ToString().getBytes(UTF_8);
204+
byte[] response = Streams.readFully(wrappedRequest).utf8ToString().getBytes(UTF_8);
203205
exchange.getResponseHeaders().add("Content-Type", "application/json");
204206
exchange.getResponseHeaders().add("Location", httpServerUrl(exchange) + "/upload/storage/v1/b/" + bucket + "/o?"
205207
+ "uploadType=resumable"
@@ -225,7 +227,7 @@ public void handle(final HttpExchange exchange) throws IOException {
225227
final int end = getContentRangeEnd(range);
226228

227229
final ByteArrayOutputStream out = new ByteArrayOutputStream();
228-
long bytesRead = Streams.copy(exchange.getRequestBody(), out);
230+
long bytesRead = Streams.copy(wrappedRequest, out);
229231
int length = Math.max(end + 1, limit != null ? limit : 0);
230232
if ((int) bytesRead > length) {
231233
throw new AssertionError("Requesting more bytes than available for blob");
@@ -250,6 +252,8 @@ public void handle(final HttpExchange exchange) throws IOException {
250252
exchange.sendResponseHeaders(RestStatus.INTERNAL_SERVER_ERROR.getStatus(), -1);
251253
}
252254
} finally {
255+
int read = exchange.getRequestBody().read();
256+
assert read == -1 : "Request body should have been fully read here but saw [" + read + "]";
253257
exchange.close();
254258
}
255259
}

test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESMockAPIBasedRepositoryIntegTestCase.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,26 @@ protected ErroneousHttpHandler(final HttpHandler delegate, final int maxErrorsPe
190190

191191
@Override
192192
public void handle(final HttpExchange exchange) throws IOException {
193-
final String requestId = requestUniqueId(exchange);
194-
assert Strings.hasText(requestId);
195-
196-
final boolean canFailRequest = canFailRequest(exchange);
197-
final int count = requests.computeIfAbsent(requestId, req -> new AtomicInteger(0)).incrementAndGet();
198-
if (count >= maxErrorsPerRequest || canFailRequest == false) {
199-
requests.remove(requestId);
200-
delegate.handle(exchange);
201-
} else {
202-
handleAsError(exchange);
193+
try {
194+
final String requestId = requestUniqueId(exchange);
195+
assert Strings.hasText(requestId);
196+
197+
final boolean canFailRequest = canFailRequest(exchange);
198+
final int count = requests.computeIfAbsent(requestId, req -> new AtomicInteger(0)).incrementAndGet();
199+
if (count >= maxErrorsPerRequest || canFailRequest == false) {
200+
requests.remove(requestId);
201+
delegate.handle(exchange);
202+
} else {
203+
handleAsError(exchange);
204+
}
205+
} finally {
206+
try {
207+
int read = exchange.getRequestBody().read();
208+
assert read == -1 : "Request body should have been fully read here but saw [" + read + "]";
209+
} catch (IOException e) {
210+
// ignored, stream is assumed to have been closed by previous handler
211+
}
212+
exchange.close();
203213
}
204214
}
205215

0 commit comments

Comments
 (0)