Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.storage;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.concurrent.ThreadSafe;

/**
* Utilities for reporting {@link TrackedFeature client feature usage} to the service.
*
* <p>Encoding contract (shared across GCS SDKs):
*
* <ul>
* <li>The set of used features is a 32-bit mask, the bitwise OR of {@link
* TrackedFeature#bitMask()} for every feature used.
* <li>The mask is serialized as 4 bytes big-endian, then encoded with URL-safe base64 (RFC 4648
* §5) <em>with</em> padding. The service rejects {@code +} and {@code /}.
* <li>When the mask is {@code 0} the header is omitted entirely.
* </ul>
*/
final class FeatureTracking {

/** Name of the request header / gRPC metadata key carrying the encoded feature mask. */
static final String HEADER = "x-goog-storage-java-features";

private static final Base64.Encoder ENCODER = Base64.getUrlEncoder();

private FeatureTracking() {}

/** Encodes {@code mask} as a 4 byte big-endian uint32, URL-safe base64 with padding. */
@VisibleForTesting
static String encode(int mask) {
return ENCODER.encodeToString(Ints.toByteArray(mask));
}
Comment on lines +52 to +54

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can optimize the encoding process by avoiding the allocation of a ByteBuffer object. Since we are dealing with a 32-bit integer, we can directly extract the bytes using bitwise shifts.

  static String encode(int mask) {
    byte[] bytes = new byte[] {
      (byte) (mask >>> 24),
      (byte) (mask >>> 16),
      (byte) (mask >>> 8),
      (byte) mask
    };
    return ENCODER.encodeToString(bytes);
  }


/**
* Returns the header map to merge into a request's extra headers: an empty map when {@code mask
* == 0}, otherwise a single entry map for {@link #HEADER}.
*/
private static ImmutableMap<String, List<String>> headers(int mask) {
if (mask == 0) {
return ImmutableMap.of();
}
return ImmutableMap.of(HEADER, ImmutableList.of(encode(mask)));
}

/** Monotonically growing feature bitmask. Bits can be set but never cleared. */
@ThreadSafe
static final class Mask {
private final AtomicInteger bits = new AtomicInteger();

void set(TrackedFeature feature) {
bits.accumulateAndGet(feature.bitMask(), (a, b) -> a | b);
}

@VisibleForTesting
int get() {
return bits.get();
}

/** Returns the headers to attach to a request, see {@link FeatureTracking#headers(int)}. */
ImmutableMap<String, List<String>> headers() {
return FeatureTracking.headers(bits.get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ final class ObjectReadSessionState {
private final AtomicReference<@MonotonicNonNull Object> metadata;
private final AtomicLong readIdSeq;

/** Shared between a parent state and all of its children, see {@link #forkChild()}. */
private final FeatureTracking.Mask featureMask;

@GuardedBy("this.lock") // https://errorprone.info/bugpattern/GuardedBy
private final Map<Long, ObjectReadSessionStreamRead<?>> outstandingReads;

Expand All @@ -69,7 +72,8 @@ final class ObjectReadSessionState {
new AtomicLong(1),
new AtomicReference<>(),
new AtomicReference<>(),
new AtomicReference<>());
new AtomicReference<>(),
new FeatureTracking.Mask());
}

private ObjectReadSessionState(
Expand All @@ -78,25 +82,37 @@ private ObjectReadSessionState(
AtomicLong readIdSeq,
AtomicReference<@Nullable BidiReadHandle> bidiReadHandle,
AtomicReference<@Nullable String> routingToken,
AtomicReference<@MonotonicNonNull Object> metadata) {
AtomicReference<@MonotonicNonNull Object> metadata,
FeatureTracking.Mask featureMask) {
this.baseContext = baseContext;
this.openRequest = openRequest;
this.bidiReadHandle = bidiReadHandle;
this.routingToken = routingToken;
this.metadata = metadata;
this.readIdSeq = readIdSeq;
this.featureMask = featureMask;
this.outstandingReads = new HashMap<>();
this.lock = new ReentrantLock();
}

/**
* Forks a new state for an additional stream of the same read session.
*
* <p>A fork means the session is now backed by more than one BidiReadObject stream, so this marks
* {@link TrackedFeature#MULTISTREAM_IN_MRD} in the feature mask shared by the parent and all
* children. Any stream opened (or re-opened) by this session afterwards carries the {@link
* FeatureTracking#HEADER} header.
*/
ObjectReadSessionState forkChild() {
featureMask.set(TrackedFeature.MULTISTREAM_IN_MRD);
return new ObjectReadSessionState(
baseContext,
openRequest,
readIdSeq,
new AtomicReference<>(bidiReadHandle.get()),
new AtomicReference<>(routingToken.get()),
new AtomicReference<>(metadata.get()));
new AtomicReference<>(metadata.get()),
featureMask);
}

boolean canHandleNewRead(ObjectReadSessionStreamRead<?> newRead) {
Expand Down Expand Up @@ -136,14 +152,17 @@ OpenArguments getOpenArguments() {
.forEach(b::addReadRanges);

ImmutableMap<String, List<String>> headers =
ImmutableMap.of(
"x-goog-request-params",
ImmutableList.of(
Stream.of(
"bucket=" + spec.getBucket(),
routingToken != null ? "routing_token=" + routingToken : null)
.filter(Objects::nonNull)
.collect(Collectors.joining("&"))));
ImmutableMap.<String, List<String>>builder()
.put(
"x-goog-request-params",
ImmutableList.of(
Stream.of(
"bucket=" + spec.getBucket(),
routingToken != null ? "routing_token=" + routingToken : null)
.filter(Objects::nonNull)
.collect(Collectors.joining("&"))))
.putAll(featureMask.headers())
.build();
return OpenArguments.of(baseContext.withExtraHeaders(headers), b.build());
} finally {
lock.unlock();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.storage;

/**
* A client feature whose usage is reported to the service via the {@link FeatureTracking#HEADER
* x-goog-storage-java-features} request header.
*
* <p>Each feature is assigned a stable bit position in a 32-bit mask. Bit positions are specific to
* the Java SDK but MUST NOT be renumbered or reused once released. This enum is the single source
* of truth and its constants are parsed by external tooling; the {@code NAME(bit)} shape of each
* constant must stay intact.
*/
enum TrackedFeature {
/**
* A {@link BlobReadSession} opened more than one BidiReadObject stream to serve concurrent reads
* which could not share a single stream.
*/
MULTISTREAM_IN_MRD(0);

private final int bitPosition;

TrackedFeature(int bitPosition) {
this.bitPosition = bitPosition;
}

/** Returns the 32-bit mask with only this feature's bit set. */
int bitMask() {
return 1 << bitPosition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,7 @@ static final class Headers extends RpcOptVal<ImmutableMap<String, String>>
"X-Goog-Encryption-Key-Sha256",
"X-Goog-Gcs-Idempotency-Token",
"X-Goog-Request-Params",
FeatureTracking.HEADER,
"X-Goog-User-Project",
"X-HTTP-Method-Override",
"X-Upload-Content-Length",
Expand Down
Loading
Loading