Skip to content

Commit b3959b6

Browse files
authored
Implement test for GCS metrics (#122909)
1 parent de41d57 commit b3959b6

File tree

7 files changed

+653
-108
lines changed

7 files changed

+653
-108
lines changed

modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ OptionalBytesReference compareAndExchangeRegister(
710710
Storage.BlobTargetOption.generationMatch()
711711
)
712712
);
713+
stats.trackPostOperation();
713714
return OptionalBytesReference.of(expected);
714715
} catch (Exception e) {
715716
final var serviceException = unwrapServiceException(e);
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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.repositories.gcs;
11+
12+
import fixture.gcs.FakeOAuth2HttpHandler;
13+
import fixture.gcs.GoogleCloudStorageHttpHandler;
14+
15+
import com.google.auth.oauth2.ServiceAccountCredentials;
16+
import com.sun.net.httpserver.HttpServer;
17+
18+
import org.elasticsearch.common.BackoffPolicy;
19+
import org.elasticsearch.common.blobstore.BlobPath;
20+
import org.elasticsearch.common.blobstore.BlobStoreActionStats;
21+
import org.elasticsearch.common.blobstore.support.BlobContainerUtils;
22+
import org.elasticsearch.common.blobstore.support.BlobMetadata;
23+
import org.elasticsearch.common.bytes.BytesArray;
24+
import org.elasticsearch.common.io.Streams;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.common.unit.ByteSizeValue;
27+
import org.elasticsearch.common.util.BigArrays;
28+
import org.elasticsearch.core.IOUtils;
29+
import org.elasticsearch.core.SuppressForbidden;
30+
import org.elasticsearch.core.TimeValue;
31+
import org.elasticsearch.core.Tuple;
32+
import org.elasticsearch.mocksocket.MockHttpServer;
33+
import org.elasticsearch.test.ESTestCase;
34+
import org.elasticsearch.threadpool.TestThreadPool;
35+
import org.elasticsearch.threadpool.ThreadPool;
36+
import org.junit.After;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
40+
import java.io.Closeable;
41+
import java.io.InputStream;
42+
import java.net.InetAddress;
43+
import java.net.InetSocketAddress;
44+
import java.net.URI;
45+
import java.util.Map;
46+
import java.util.concurrent.TimeUnit;
47+
48+
import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomPurpose;
49+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.APPLICATION_NAME_SETTING;
50+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CONNECT_TIMEOUT_SETTING;
51+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.PROJECT_ID_SETTING;
52+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.READ_TIMEOUT_SETTING;
53+
54+
@SuppressForbidden(reason = "Uses a HttpServer to emulate a Google Cloud Storage endpoint")
55+
public class GoogleCloudStorageBlobContainerStatsTests extends ESTestCase {
56+
private static final String BUCKET = "bucket";
57+
private static final ByteSizeValue BUFFER_SIZE = ByteSizeValue.ofKb(128);
58+
59+
private HttpServer httpServer;
60+
private ThreadPool threadPool;
61+
private GoogleCloudStorageService googleCloudStorageService;
62+
private GoogleCloudStorageHttpHandler googleCloudStorageHttpHandler;
63+
private ContainerAndBlobStore containerAndStore;
64+
65+
@Before
66+
public void createStorageService() throws Exception {
67+
threadPool = new TestThreadPool(getTestClass().getName());
68+
httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
69+
httpServer.start();
70+
googleCloudStorageService = new GoogleCloudStorageService();
71+
googleCloudStorageHttpHandler = new GoogleCloudStorageHttpHandler(BUCKET);
72+
httpServer.createContext("/", googleCloudStorageHttpHandler);
73+
httpServer.createContext("/token", new FakeOAuth2HttpHandler());
74+
containerAndStore = createBlobContainer(randomIdentifier());
75+
}
76+
77+
@After
78+
public void stopHttpServer() {
79+
IOUtils.closeWhileHandlingException(containerAndStore);
80+
httpServer.stop(0);
81+
ThreadPool.terminate(threadPool, 10L, TimeUnit.SECONDS);
82+
}
83+
84+
@Test
85+
public void testSingleMultipartWrite() throws Exception {
86+
final GoogleCloudStorageBlobContainer container = containerAndStore.blobContainer();
87+
final GoogleCloudStorageBlobStore store = containerAndStore.blobStore();
88+
89+
final String blobName = randomIdentifier();
90+
final int blobLength = randomIntBetween(1, (int) store.getLargeBlobThresholdInBytes() - 1);
91+
final BytesArray blobContents = new BytesArray(randomByteArrayOfLength(blobLength));
92+
container.writeBlob(randomPurpose(), blobName, blobContents, true);
93+
assertEquals(createStats(1, 0, 0), store.stats());
94+
95+
try (InputStream is = container.readBlob(randomPurpose(), blobName)) {
96+
assertEquals(blobContents, Streams.readFully(is));
97+
}
98+
assertEquals(createStats(1, 0, 1), store.stats());
99+
}
100+
101+
@Test
102+
public void testResumableWrite() throws Exception {
103+
final GoogleCloudStorageBlobContainer container = containerAndStore.blobContainer();
104+
final GoogleCloudStorageBlobStore store = containerAndStore.blobStore();
105+
106+
final String blobName = randomIdentifier();
107+
final int size = randomIntBetween((int) store.getLargeBlobThresholdInBytes(), (int) store.getLargeBlobThresholdInBytes() * 2);
108+
final BytesArray blobContents = new BytesArray(randomByteArrayOfLength(size));
109+
container.writeBlob(randomPurpose(), blobName, blobContents, true);
110+
assertEquals(createStats(1, 0, 0), store.stats());
111+
112+
try (InputStream is = container.readBlob(randomPurpose(), blobName)) {
113+
assertEquals(blobContents, Streams.readFully(is));
114+
}
115+
assertEquals(createStats(1, 0, 1), store.stats());
116+
}
117+
118+
@Test
119+
public void testDeleteDirectory() throws Exception {
120+
final GoogleCloudStorageBlobContainer container = containerAndStore.blobContainer();
121+
final GoogleCloudStorageBlobStore store = containerAndStore.blobStore();
122+
123+
final String directoryName = randomIdentifier();
124+
final BytesArray contents = new BytesArray(randomByteArrayOfLength(50));
125+
final int numberOfFiles = randomIntBetween(1, 20);
126+
for (int i = 0; i < numberOfFiles; i++) {
127+
container.writeBlob(randomPurpose(), String.format("%s/file_%d", directoryName, i), contents, true);
128+
}
129+
assertEquals(createStats(numberOfFiles, 0, 0), store.stats());
130+
131+
container.delete(randomPurpose());
132+
// We only count the list because we can't track the bulk delete
133+
assertEquals(createStats(numberOfFiles, 1, 0), store.stats());
134+
}
135+
136+
@Test
137+
public void testListBlobsAccountsForPaging() throws Exception {
138+
final GoogleCloudStorageBlobContainer container = containerAndStore.blobContainer();
139+
final GoogleCloudStorageBlobStore store = containerAndStore.blobStore();
140+
141+
final int pageSize = randomIntBetween(3, 20);
142+
googleCloudStorageHttpHandler.setDefaultPageLimit(pageSize);
143+
final int numberOfPages = randomIntBetween(1, 10);
144+
final int numberOfObjects = randomIntBetween((numberOfPages - 1) * pageSize, numberOfPages * pageSize - 1);
145+
final BytesArray contents = new BytesArray(randomByteArrayOfLength(50));
146+
for (int i = 0; i < numberOfObjects; i++) {
147+
container.writeBlob(randomPurpose(), String.format("file_%d", i), contents, true);
148+
}
149+
assertEquals(createStats(numberOfObjects, 0, 0), store.stats());
150+
151+
final Map<String, BlobMetadata> stringBlobMetadataMap = container.listBlobs(randomPurpose());
152+
assertEquals(numberOfObjects, stringBlobMetadataMap.size());
153+
// There should be {numberOfPages} pages of blobs
154+
assertEquals(createStats(numberOfObjects, numberOfPages, 0), store.stats());
155+
}
156+
157+
public void testCompareAndSetRegister() {
158+
final GoogleCloudStorageBlobContainer container = containerAndStore.blobContainer();
159+
final GoogleCloudStorageBlobStore store = containerAndStore.blobStore();
160+
161+
// update from empty (adds a single insert)
162+
final BytesArray contents = new BytesArray(randomByteArrayOfLength(BlobContainerUtils.MAX_REGISTER_CONTENT_LENGTH));
163+
final String registerName = randomIdentifier();
164+
assertTrue(safeAwait(l -> container.compareAndSetRegister(randomPurpose(), registerName, BytesArray.EMPTY, contents, l)));
165+
assertEquals(createStats(1, 0, 0), store.stats());
166+
167+
// successful update from non-null (adds two gets, one insert)
168+
final BytesArray nextContents = new BytesArray(randomByteArrayOfLength(BlobContainerUtils.MAX_REGISTER_CONTENT_LENGTH));
169+
assertTrue(safeAwait(l -> container.compareAndSetRegister(randomPurpose(), registerName, contents, nextContents, l)));
170+
assertEquals(createStats(2, 0, 2), store.stats());
171+
172+
// failed update (adds two gets, zero inserts)
173+
final BytesArray wrongContents = randomValueOtherThan(
174+
nextContents,
175+
() -> new BytesArray(randomByteArrayOfLength(BlobContainerUtils.MAX_REGISTER_CONTENT_LENGTH))
176+
);
177+
assertFalse(safeAwait(l -> container.compareAndSetRegister(randomPurpose(), registerName, wrongContents, contents, l)));
178+
assertEquals(createStats(2, 0, 4), store.stats());
179+
}
180+
181+
private Map<String, BlobStoreActionStats> createStats(int insertCount, int listCount, int getCount) {
182+
return Map.of(
183+
"GetObject",
184+
new BlobStoreActionStats(getCount, getCount),
185+
"ListObjects",
186+
new BlobStoreActionStats(listCount, listCount),
187+
"InsertObject",
188+
new BlobStoreActionStats(insertCount, insertCount)
189+
);
190+
}
191+
192+
private record ContainerAndBlobStore(GoogleCloudStorageBlobContainer blobContainer, GoogleCloudStorageBlobStore blobStore)
193+
implements
194+
Closeable {
195+
196+
@Override
197+
public void close() {
198+
blobStore.close();
199+
}
200+
}
201+
202+
private ContainerAndBlobStore createBlobContainer(final String repositoryName) throws Exception {
203+
final String clientName = randomIdentifier();
204+
205+
final Tuple<ServiceAccountCredentials, byte[]> serviceAccountCredentialsTuple = GoogleCloudStorageTestUtilities.randomCredential(
206+
clientName
207+
);
208+
final GoogleCloudStorageClientSettings clientSettings = new GoogleCloudStorageClientSettings(
209+
serviceAccountCredentialsTuple.v1(),
210+
getEndpointForServer(httpServer),
211+
PROJECT_ID_SETTING.getDefault(Settings.EMPTY),
212+
CONNECT_TIMEOUT_SETTING.getDefault(Settings.EMPTY),
213+
READ_TIMEOUT_SETTING.getDefault(Settings.EMPTY),
214+
APPLICATION_NAME_SETTING.getDefault(Settings.EMPTY),
215+
new URI(getEndpointForServer(httpServer) + "/token"),
216+
null
217+
);
218+
googleCloudStorageService.refreshAndClearCache(Map.of(clientName, clientSettings));
219+
final GoogleCloudStorageBlobStore blobStore = new GoogleCloudStorageBlobStore(
220+
BUCKET,
221+
clientName,
222+
repositoryName,
223+
googleCloudStorageService,
224+
BigArrays.NON_RECYCLING_INSTANCE,
225+
Math.toIntExact(BUFFER_SIZE.getBytes()),
226+
BackoffPolicy.constantBackoff(TimeValue.timeValueMillis(10), 10)
227+
);
228+
final GoogleCloudStorageBlobContainer googleCloudStorageBlobContainer = new GoogleCloudStorageBlobContainer(
229+
BlobPath.EMPTY,
230+
blobStore
231+
);
232+
return new ContainerAndBlobStore(googleCloudStorageBlobContainer, blobStore);
233+
}
234+
235+
protected String getEndpointForServer(final HttpServer server) {
236+
final InetSocketAddress address = server.getAddress();
237+
return "http://" + address.getHostString() + ":" + address.getPort();
238+
}
239+
}

modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageClientSettingsTests.java

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99
package org.elasticsearch.repositories.gcs;
1010

11-
import com.google.api.services.storage.StorageScopes;
1211
import com.google.auth.oauth2.ServiceAccountCredentials;
1312

1413
import org.apache.http.HttpRequest;
@@ -29,11 +28,9 @@
2928
import java.net.Proxy;
3029
import java.net.URI;
3130
import java.nio.charset.StandardCharsets;
32-
import java.security.KeyPair;
3331
import java.security.KeyPairGenerator;
3432
import java.util.ArrayList;
3533
import java.util.Base64;
36-
import java.util.Collections;
3734
import java.util.HashMap;
3835
import java.util.List;
3936
import java.util.Locale;
@@ -47,6 +44,7 @@
4744
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.READ_TIMEOUT_SETTING;
4845
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.getClientSettings;
4946
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.loadCredential;
47+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageTestUtilities.randomCredential;
5048
import static org.hamcrest.Matchers.equalTo;
5149

5250
public class GoogleCloudStorageClientSettingsTests extends ESTestCase {
@@ -292,32 +290,6 @@ private static GoogleCloudStorageClientSettings randomClient(
292290
);
293291
}
294292

295-
/** Generates a random GoogleCredential along with its corresponding Service Account file provided as a byte array **/
296-
private static Tuple<ServiceAccountCredentials, byte[]> randomCredential(final String clientName) throws Exception {
297-
final KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
298-
final ServiceAccountCredentials.Builder credentialBuilder = ServiceAccountCredentials.newBuilder();
299-
credentialBuilder.setClientId("id_" + clientName);
300-
credentialBuilder.setClientEmail(clientName);
301-
credentialBuilder.setProjectId("project_id_" + clientName);
302-
credentialBuilder.setPrivateKey(keyPair.getPrivate());
303-
credentialBuilder.setPrivateKeyId("private_key_id_" + clientName);
304-
credentialBuilder.setScopes(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL));
305-
URI tokenServerUri = URI.create("http://localhost/oauth2/token");
306-
credentialBuilder.setTokenServerUri(tokenServerUri);
307-
final String encodedPrivateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
308-
final String serviceAccount = Strings.format("""
309-
{
310-
"type": "service_account",
311-
"project_id": "project_id_%s",
312-
"private_key_id": "private_key_id_%s",
313-
"private_key": "-----BEGIN PRIVATE KEY-----\\n%s\\n-----END PRIVATE KEY-----\\n",
314-
"client_email": "%s",
315-
"client_id": "id_%s",
316-
"token_uri": "%s"
317-
}""", clientName, clientName, encodedPrivateKey, clientName, clientName, tokenServerUri);
318-
return Tuple.tuple(credentialBuilder.build(), serviceAccount.getBytes(StandardCharsets.UTF_8));
319-
}
320-
321293
private static TimeValue randomTimeout() {
322294
return randomFrom(TimeValue.MINUS_ONE, TimeValue.ZERO, randomPositiveTimeValue());
323295
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.repositories.gcs;
11+
12+
import com.google.api.services.storage.StorageScopes;
13+
import com.google.auth.oauth2.ServiceAccountCredentials;
14+
15+
import org.elasticsearch.core.Strings;
16+
import org.elasticsearch.core.Tuple;
17+
18+
import java.net.URI;
19+
import java.nio.charset.StandardCharsets;
20+
import java.security.KeyPair;
21+
import java.security.KeyPairGenerator;
22+
import java.util.Base64;
23+
import java.util.Collections;
24+
25+
public class GoogleCloudStorageTestUtilities {
26+
27+
/** Generates a random GoogleCredential along with its corresponding Service Account file provided as a byte array **/
28+
public static Tuple<ServiceAccountCredentials, byte[]> randomCredential(final String clientName) throws Exception {
29+
final KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
30+
final ServiceAccountCredentials.Builder credentialBuilder = ServiceAccountCredentials.newBuilder();
31+
credentialBuilder.setClientId("id_" + clientName);
32+
credentialBuilder.setClientEmail(clientName);
33+
credentialBuilder.setProjectId("project_id_" + clientName);
34+
credentialBuilder.setPrivateKey(keyPair.getPrivate());
35+
credentialBuilder.setPrivateKeyId("private_key_id_" + clientName);
36+
credentialBuilder.setScopes(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL));
37+
URI tokenServerUri = URI.create("http://localhost/oauth2/token");
38+
credentialBuilder.setTokenServerUri(tokenServerUri);
39+
final String encodedPrivateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
40+
final String serviceAccount = Strings.format("""
41+
{
42+
"type": "service_account",
43+
"project_id": "project_id_%s",
44+
"private_key_id": "private_key_id_%s",
45+
"private_key": "-----BEGIN PRIVATE KEY-----\\n%s\\n-----END PRIVATE KEY-----\\n",
46+
"client_email": "%s",
47+
"client_id": "id_%s",
48+
"token_uri": "%s"
49+
}""", clientName, clientName, encodedPrivateKey, clientName, clientName, tokenServerUri);
50+
return Tuple.tuple(credentialBuilder.build(), serviceAccount.getBytes(StandardCharsets.UTF_8));
51+
}
52+
}

0 commit comments

Comments
 (0)