Skip to content

Commit 053b037

Browse files
authored
GCS blob store: add OperationPurpose/Operation stats counters (#122991)
1 parent dd2a5c6 commit 053b037

File tree

10 files changed

+383
-186
lines changed

10 files changed

+383
-186
lines changed

docs/changelog/122991.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 122991
2+
summary: "GCS blob store: add `OperationPurpose/Operation` stats counters"
3+
area: Snapshot/Restore
4+
type: enhancement
5+
issues: []

modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CREDENTIALS_FILE_SETTING;
6464
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING;
6565
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.TOKEN_URI_SETTING;
66+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageOperationsStats.Operation;
6667
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BASE_PATH;
6768
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BUCKET;
6869
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.CLIENT_NAME;
@@ -212,8 +213,8 @@ public TestGoogleCloudStoragePlugin(Settings settings) {
212213
}
213214

214215
@Override
215-
protected GoogleCloudStorageService createStorageService() {
216-
return new GoogleCloudStorageService() {
216+
protected GoogleCloudStorageService createStorageService(Settings settings) {
217+
return new GoogleCloudStorageService(settings) {
217218
@Override
218219
StorageOptions createStorageOptions(
219220
final GoogleCloudStorageClientSettings gcsClientSettings,
@@ -346,19 +347,17 @@ private static class GoogleCloudStorageStatsCollectorHttpHandler extends HttpSta
346347

347348
@Override
348349
public void maybeTrack(final String request, Headers requestHeaders) {
349-
if (Regex.simpleMatch("GET /storage/v1/b/*/o/*", request)) {
350-
trackRequest("GetObject");
350+
if (Regex.simpleMatch("GET */storage/v1/b/*/o/*", request)) {
351+
trackRequest(Operation.GET_OBJECT.key());
351352
} else if (Regex.simpleMatch("GET /storage/v1/b/*/o*", request)) {
352-
trackRequest("ListObjects");
353-
} else if (Regex.simpleMatch("GET /download/storage/v1/b/*", request)) {
354-
trackRequest("GetObject");
353+
trackRequest(Operation.LIST_OBJECTS.key());
355354
} else if (Regex.simpleMatch("PUT /upload/storage/v1/b/*uploadType=resumable*", request) && isLastPart(requestHeaders)) {
356355
// Resumable uploads are billed as a single operation, that's the reason we're tracking
357356
// the request only when it's the last part.
358357
// See https://cloud.google.com/storage/docs/resumable-uploads#introduction
359-
trackRequest("InsertObject");
358+
trackRequest(Operation.INSERT_OBJECT.key());
360359
} else if (Regex.simpleMatch("POST /upload/storage/v1/b/*uploadType=multipart*", request)) {
361-
trackRequest("InsertObject");
360+
trackRequest(Operation.INSERT_OBJECT.key());
362361
}
363362
}
364363

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.elasticsearch.core.Streams;
4545
import org.elasticsearch.core.SuppressForbidden;
4646
import org.elasticsearch.core.TimeValue;
47+
import org.elasticsearch.repositories.gcs.GoogleCloudStorageOperationsStats.Operation;
4748
import org.elasticsearch.rest.RestStatus;
4849

4950
import java.io.ByteArrayInputStream;
@@ -73,6 +74,11 @@
7374

7475
class GoogleCloudStorageBlobStore implements BlobStore {
7576

77+
/**
78+
* see com.google.cloud.BaseWriteChannel#DEFAULT_CHUNK_SIZE
79+
*/
80+
static final int SDK_DEFAULT_CHUNK_SIZE = 60 * 256 * 1024;
81+
7682
private static final Logger logger = LogManager.getLogger(GoogleCloudStorageBlobStore.class);
7783

7884
// The recommended maximum size of a blob that should be uploaded in a single
@@ -124,7 +130,7 @@ class GoogleCloudStorageBlobStore implements BlobStore {
124130
this.repositoryName = repositoryName;
125131
this.storageService = storageService;
126132
this.bigArrays = bigArrays;
127-
this.stats = new GoogleCloudStorageOperationsStats(bucketName);
133+
this.stats = new GoogleCloudStorageOperationsStats(bucketName, storageService.isStateless());
128134
this.bufferSize = bufferSize;
129135
this.casBackoffPolicy = casBackoffPolicy;
130136
}
@@ -378,9 +384,7 @@ private void initResumableStream() throws IOException {
378384
public void write(byte[] b, int off, int len) throws IOException {
379385
int written = 0;
380386
while (written < len) {
381-
// at most write the default chunk size in one go to prevent allocating huge buffers in the SDK
382-
// see com.google.cloud.BaseWriteChannel#DEFAULT_CHUNK_SIZE
383-
final int toWrite = Math.min(len - written, 60 * 256 * 1024);
387+
final int toWrite = Math.min(len - written, SDK_DEFAULT_CHUNK_SIZE);
384388
out.write(b, off + written, toWrite);
385389
written += toWrite;
386390
}
@@ -393,7 +397,7 @@ public void write(byte[] b, int off, int len) throws IOException {
393397
final WritableByteChannel writeChannel = channelRef.get();
394398
if (writeChannel != null) {
395399
SocketAccess.doPrivilegedVoidIOException(writeChannel::close);
396-
stats.trackPutOperation();
400+
stats.tracker().trackOperation(purpose, Operation.INSERT_OBJECT);
397401
} else {
398402
writeBlob(purpose, blobName, buffer.bytes(), failIfAlreadyExists);
399403
}
@@ -463,7 +467,7 @@ private void writeBlobResumable(
463467
// we do with the GET/LIST operations since this operations
464468
// can trigger multiple underlying http requests but only one
465469
// operation is billed.
466-
stats.trackPutOperation();
470+
stats.tracker().trackOperation(purpose, Operation.INSERT_OBJECT);
467471
return;
468472
} catch (final StorageException se) {
469473
final int errorCode = se.getCode();
@@ -515,7 +519,7 @@ private void writeBlobMultipart(
515519
// we do with the GET/LIST operations since this operations
516520
// can trigger multiple underlying http requests but only one
517521
// operation is billed.
518-
stats.trackPostOperation();
522+
stats.tracker().trackOperation(purpose, Operation.INSERT_OBJECT);
519523
} catch (final StorageException se) {
520524
if (failIfAlreadyExists && se.getCode() == HTTP_PRECON_FAILED) {
521525
throw new FileAlreadyExistsException(blobInfo.getBlobId().getName(), null, se.getMessage());
@@ -634,7 +638,7 @@ private static String buildKey(String keyPath, String s) {
634638

635639
@Override
636640
public Map<String, BlobStoreActionStats> stats() {
637-
return stats.toMap();
641+
return stats.tracker().toMap();
638642
}
639643

640644
private static final class WritableBlobChannel implements WritableByteChannel {
@@ -745,7 +749,7 @@ OptionalBytesReference compareAndExchangeRegister(
745749
Storage.BlobTargetOption.generationMatch()
746750
)
747751
);
748-
stats.trackPostOperation();
752+
stats.tracker().trackOperation(purpose, Operation.INSERT_OBJECT);
749753
return OptionalBytesReference.of(expected);
750754
} catch (Exception e) {
751755
final var serviceException = unwrapServiceException(e);

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

Lines changed: 62 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -9,115 +9,88 @@
99

1010
package org.elasticsearch.repositories.gcs;
1111

12-
import com.google.api.client.http.GenericUrl;
13-
import com.google.api.client.http.HttpRequest;
1412
import com.google.api.client.http.HttpResponse;
1513
import com.google.api.client.http.HttpResponseInterceptor;
1614

1715
import org.elasticsearch.common.blobstore.OperationPurpose;
16+
import org.elasticsearch.logging.LogManager;
17+
import org.elasticsearch.logging.Logger;
1818

19-
import java.util.List;
20-
import java.util.Locale;
21-
import java.util.function.Consumer;
22-
import java.util.function.Function;
2319
import java.util.regex.Pattern;
2420

25-
import static java.lang.String.format;
21+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageOperationsStats.Operation;
22+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageOperationsStats.StatsTracker;
2623

2724
final class GoogleCloudStorageHttpStatsCollector implements HttpResponseInterceptor {
28-
// The specification for the current API (v1) endpoints can be found at:
29-
// https://cloud.google.com/storage/docs/json_api/v1
30-
private static final List<Function<String, HttpRequestTracker>> trackerFactories = List.of(
31-
(bucket) -> HttpRequestTracker.get(
32-
format(Locale.ROOT, "/download/storage/v1/b/%s/o/.+", bucket),
33-
GoogleCloudStorageOperationsStats::trackGetOperation
34-
),
3525

36-
(bucket) -> HttpRequestTracker.get(
37-
format(Locale.ROOT, "/storage/v1/b/%s/o/.+", bucket),
38-
GoogleCloudStorageOperationsStats::trackGetOperation
39-
),
26+
private static final Logger logger = LogManager.getLogger("GcpHttpStats");
4027

41-
(bucket) -> HttpRequestTracker.get(
42-
format(Locale.ROOT, "/storage/v1/b/%s/o", bucket),
43-
GoogleCloudStorageOperationsStats::trackListOperation
44-
)
45-
);
28+
private final StatsTracker stats;
29+
private final OperationPurpose purpose;
30+
private final Pattern getObjPattern;
31+
private final Pattern insertObjPattern;
32+
private final Pattern listObjPattern;
4633

47-
private final GoogleCloudStorageOperationsStats gcsOperationStats;
48-
private final OperationPurpose operationPurpose;
49-
private final List<HttpRequestTracker> trackers;
34+
GoogleCloudStorageHttpStatsCollector(final GoogleCloudStorageOperationsStats stats, OperationPurpose purpose) {
35+
this.stats = stats.tracker();
36+
this.purpose = purpose;
37+
var bucket = stats.bucketName();
5038

51-
GoogleCloudStorageHttpStatsCollector(final GoogleCloudStorageOperationsStats gcsOperationStats, OperationPurpose operationPurpose) {
52-
this.gcsOperationStats = gcsOperationStats;
53-
this.operationPurpose = operationPurpose;
54-
this.trackers = trackerFactories.stream()
55-
.map(trackerFactory -> trackerFactory.apply(gcsOperationStats.getTrackedBucket()))
56-
.toList();
39+
// The specification for the current API (v1) endpoints can be found at:
40+
// https://cloud.google.com/storage/docs/json_api/v1
41+
this.getObjPattern = Pattern.compile("(/download)?/storage/v1/b/" + bucket + "/o/.+");
42+
this.insertObjPattern = Pattern.compile("(/upload)?/storage/v1/b/" + bucket + "/o");
43+
this.listObjPattern = Pattern.compile("/storage/v1/b/" + bucket + "/o");
5744
}
5845

59-
@Override
60-
public void interceptResponse(final HttpResponse response) {
61-
// TODO keep track of unsuccessful requests in different entries
62-
if (response.isSuccessStatusCode() == false) return;
63-
64-
final HttpRequest request = response.getRequest();
65-
for (HttpRequestTracker tracker : trackers) {
66-
if (tracker.track(request, gcsOperationStats)) {
67-
return;
68-
}
69-
}
46+
private void trackRequest(Operation operation) {
47+
stats.trackRequest(purpose, operation);
7048
}
7149

72-
/**
73-
* Http request tracker that allows to track certain HTTP requests based on the following criteria:
74-
* <ul>
75-
* <li>The HTTP request method</li>
76-
* <li>An URI path regex expression</li>
77-
* </ul>
78-
*
79-
* The requests that match the previous criteria are tracked using the {@code statsTracker} function.
80-
*/
81-
private static final class HttpRequestTracker {
82-
private final String method;
83-
private final Pattern pathPattern;
84-
private final Consumer<GoogleCloudStorageOperationsStats> statsTracker;
85-
86-
private HttpRequestTracker(
87-
final String method,
88-
final String pathPattern,
89-
final Consumer<GoogleCloudStorageOperationsStats> statsTracker
90-
) {
91-
this.method = method;
92-
this.pathPattern = Pattern.compile(pathPattern);
93-
this.statsTracker = statsTracker;
94-
}
95-
96-
private static HttpRequestTracker get(final String pathPattern, final Consumer<GoogleCloudStorageOperationsStats> statsConsumer) {
97-
return new HttpRequestTracker("GET", pathPattern, statsConsumer);
98-
}
99-
100-
/**
101-
* Tracks the provided http request if it matches the criteria defined by this tracker.
102-
*
103-
* @param httpRequest the http request to be tracked
104-
* @param stats the operation tracker
105-
*
106-
* @return {@code true} if the http request was tracked, {@code false} otherwise.
107-
*/
108-
private boolean track(final HttpRequest httpRequest, final GoogleCloudStorageOperationsStats stats) {
109-
if (matchesCriteria(httpRequest) == false) return false;
50+
private void trackRequestAndOperation(Operation operation) {
51+
stats.trackOperationAndRequest(purpose, operation);
52+
}
11053

111-
statsTracker.accept(stats);
112-
return true;
54+
@Override
55+
public void interceptResponse(final HttpResponse response) {
56+
var respCode = response.getStatusCode();
57+
// Some of the intermediate and error codes are still counted as "good" requests
58+
if (((respCode >= 200 && respCode < 300) || respCode == 308 || respCode == 404) == false) {
59+
return;
11360
}
114-
115-
private boolean matchesCriteria(final HttpRequest httpRequest) {
116-
return method.equalsIgnoreCase(httpRequest.getRequestMethod()) && pathMatches(httpRequest.getUrl());
61+
var request = response.getRequest();
62+
63+
var path = request.getUrl().getRawPath();
64+
var ignored = false;
65+
switch (request.getRequestMethod()) {
66+
case "GET" -> {
67+
// https://cloud.google.com/storage/docs/json_api/v1/objects/get
68+
if (getObjPattern.matcher(path).matches()) {
69+
trackRequestAndOperation(Operation.GET_OBJECT);
70+
} else if (listObjPattern.matcher(path).matches()) {
71+
trackRequestAndOperation(Operation.LIST_OBJECTS);
72+
} else {
73+
ignored = true;
74+
}
75+
}
76+
case "POST", "PUT" -> {
77+
// https://cloud.google.com/storage/docs/json_api/v1/objects/insert
78+
if (insertObjPattern.matcher(path).matches()) {
79+
trackRequest(Operation.INSERT_OBJECT);
80+
} else {
81+
ignored = true;
82+
}
83+
}
84+
default -> ignored = true;
11785
}
118-
119-
private boolean pathMatches(final GenericUrl url) {
120-
return pathPattern.matcher(url.getRawPath()).matches();
86+
if (ignored) {
87+
logger.debug(
88+
"not handling request:{} {} response:{} {}",
89+
request.getRequestMethod(),
90+
path,
91+
response.getStatusCode(),
92+
response.getStatusMessage()
93+
);
12194
}
12295
}
12396
}

0 commit comments

Comments
 (0)