| 
21 | 21 | import com.sun.net.httpserver.HttpHandler;  | 
22 | 22 | 
 
  | 
23 | 23 | import org.apache.http.HttpStatus;  | 
 | 24 | +import org.apache.logging.log4j.Level;  | 
24 | 25 | import org.apache.lucene.index.CorruptIndexException;  | 
25 | 26 | import org.apache.lucene.store.AlreadyClosedException;  | 
26 | 27 | import org.elasticsearch.ExceptionsHelper;  | 
 | 
51 | 52 | import org.elasticsearch.repositories.RepositoriesMetrics;  | 
52 | 53 | import org.elasticsearch.repositories.blobstore.AbstractBlobContainerRetriesTestCase;  | 
53 | 54 | import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil;  | 
 | 55 | +import org.elasticsearch.rest.RestStatus;  | 
54 | 56 | import org.elasticsearch.telemetry.InstrumentType;  | 
55 | 57 | import org.elasticsearch.telemetry.Measurement;  | 
56 | 58 | import org.elasticsearch.telemetry.RecordingMeterRegistry;  | 
57 | 59 | import org.elasticsearch.test.ESTestCase;  | 
 | 60 | +import org.elasticsearch.test.MockLog;  | 
58 | 61 | import org.elasticsearch.watcher.ResourceWatcherService;  | 
59 | 62 | import org.hamcrest.Matcher;  | 
60 | 63 | import org.junit.After;  | 
 | 
65 | 68 | import java.io.FilterInputStream;  | 
66 | 69 | import java.io.IOException;  | 
67 | 70 | import java.io.InputStream;  | 
 | 71 | +import java.io.InputStreamReader;  | 
68 | 72 | import java.net.InetSocketAddress;  | 
69 | 73 | import java.net.SocketTimeoutException;  | 
70 | 74 | import java.net.UnknownHostException;  | 
 | 
83 | 87 | import java.util.concurrent.atomic.AtomicLong;  | 
84 | 88 | import java.util.concurrent.atomic.AtomicReference;  | 
85 | 89 | import java.util.function.IntConsumer;  | 
 | 90 | +import java.util.regex.Pattern;  | 
86 | 91 | 
 
  | 
87 | 92 | import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomNonDataPurpose;  | 
88 | 93 | import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomPurpose;  | 
@@ -1106,6 +1111,58 @@ public void testSuppressedDeletionErrorsAreCapped() {  | 
1106 | 1111 |         assertThat(exception.getCause().getSuppressed().length, lessThan(S3BlobStore.MAX_DELETE_EXCEPTIONS));  | 
1107 | 1112 |     }  | 
1108 | 1113 | 
 
  | 
 | 1114 | +    public void testTrimmedLogAndCappedSuppressedErrorOnMultiObjectDeletionException() {  | 
 | 1115 | +        final TimeValue readTimeout = TimeValue.timeValueMillis(randomIntBetween(100, 500));  | 
 | 1116 | +        int maxBulkDeleteSize = randomIntBetween(10, 20);  | 
 | 1117 | +        final BlobContainer blobContainer = createBlobContainer(1, readTimeout, true, null, maxBulkDeleteSize);  | 
 | 1118 | + | 
 | 1119 | +        final Pattern pattern = Pattern.compile("<Key>(.+?)</Key>");  | 
 | 1120 | +        httpServer.createContext("/", exchange -> {  | 
 | 1121 | +            if (exchange.getRequestMethod().equals("POST") && exchange.getRequestURI().toString().startsWith("/bucket/?delete")) {  | 
 | 1122 | +                final String requestBody = Streams.copyToString(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));  | 
 | 1123 | +                final var matcher = pattern.matcher(requestBody);  | 
 | 1124 | +                final StringBuilder deletes = new StringBuilder();  | 
 | 1125 | +                deletes.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");  | 
 | 1126 | +                deletes.append("<DeleteResult>");  | 
 | 1127 | +                while (matcher.find()) {  | 
 | 1128 | +                    final String key = matcher.group(1);  | 
 | 1129 | +                    deletes.append("<Error>");  | 
 | 1130 | +                    deletes.append("<Code>").append(randomAlphaOfLength(10)).append("</Code>");  | 
 | 1131 | +                    deletes.append("<Key>").append(key).append("</Key>");  | 
 | 1132 | +                    deletes.append("<Message>").append(randomAlphaOfLength(200)).append("</Message>");  | 
 | 1133 | +                    deletes.append("</Error>");  | 
 | 1134 | +                }  | 
 | 1135 | +                deletes.append("</DeleteResult>");  | 
 | 1136 | + | 
 | 1137 | +                byte[] response = deletes.toString().getBytes(StandardCharsets.UTF_8);  | 
 | 1138 | +                exchange.getResponseHeaders().add("Content-Type", "application/xml");  | 
 | 1139 | +                exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);  | 
 | 1140 | +                exchange.getResponseBody().write(response);  | 
 | 1141 | +                exchange.close();  | 
 | 1142 | +            } else {  | 
 | 1143 | +                fail("expected only deletions");  | 
 | 1144 | +            }  | 
 | 1145 | +        });  | 
 | 1146 | +        var blobs = randomList(maxBulkDeleteSize, maxBulkDeleteSize, ESTestCase::randomIdentifier);  | 
 | 1147 | +        try (var mockLog = MockLog.capture(S3BlobStore.class)) {  | 
 | 1148 | +            mockLog.addExpectation(  | 
 | 1149 | +                new MockLog.SeenEventExpectation(  | 
 | 1150 | +                    "deletion log",  | 
 | 1151 | +                    S3BlobStore.class.getCanonicalName(),  | 
 | 1152 | +                    Level.WARN,  | 
 | 1153 | +                    "Failed to delete some blobs [*... (* in total, * omitted)"  | 
 | 1154 | +                )  | 
 | 1155 | +            );  | 
 | 1156 | +            var exception = expectThrows(  | 
 | 1157 | +                IOException.class,  | 
 | 1158 | +                "deletion should not succeed",  | 
 | 1159 | +                () -> blobContainer.deleteBlobsIgnoringIfNotExists(randomPurpose(), blobs.iterator())  | 
 | 1160 | +            );  | 
 | 1161 | +            assertThat(exception.getCause().getSuppressed().length, lessThan(S3BlobStore.MAX_DELETE_EXCEPTIONS));  | 
 | 1162 | +            mockLog.awaitAllExpectationsMatched();  | 
 | 1163 | +        }  | 
 | 1164 | +    }  | 
 | 1165 | + | 
1109 | 1166 |     @Override  | 
1110 | 1167 |     protected Matcher<Integer> getMaxRetriesMatcher(int maxRetries) {  | 
1111 | 1168 |         // some attempts make meaningful progress and do not count towards the max retry limit  | 
 | 
0 commit comments