Skip to content

Commit 37ef7f3

Browse files
authored
feat: add support for partial success in ListBuckets for json (#3415)
2 parents 66d54e2 + dbd4864 commit 37ef7f3

File tree

10 files changed

+198
-5
lines changed

10 files changed

+198
-5
lines changed

google-cloud-storage/clirr-ignored-differences.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@
167167
<method>com.google.cloud.storage.BucketInfo$Builder setGoogleManagedEncryptionEnforcementConfig(com.google.cloud.storage.BucketInfo$GoogleManagedEncryptionEnforcementConfig)</method>
168168
</difference>
169169

170+
<difference>
171+
<differenceType>7013</differenceType>
172+
<className>com/google/cloud/storage/BucketInfo$Builder</className>
173+
<method>com.google.cloud.storage.BucketInfo$Builder setIsUnreachable(java.lang.Boolean)</method>
174+
</difference>
175+
170176
<!-- make beta api constructors private, they still retain their factory methods. -->
171177
<difference>
172178
<differenceType>7004</differenceType>

google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,12 @@ public Builder setCustomerSuppliedEncryptionEnforcementConfig(
789789
return this;
790790
}
791791

792+
@Override
793+
public Builder setIsUnreachable(Boolean isUnreachable) {
794+
infoBuilder.setIsUnreachable(isUnreachable);
795+
return this;
796+
}
797+
792798
@Override
793799
public Bucket build() {
794800
return new Bucket(storage, infoBuilder);
@@ -997,6 +1003,12 @@ public Builder clearCustomerSuppliedEncryptionEnforcementConfig() {
9971003
infoBuilder.clearCustomerSuppliedEncryptionEnforcementConfig();
9981004
return this;
9991005
}
1006+
1007+
@Override
1008+
Builder clearIsUnreachable() {
1009+
infoBuilder.clearIsUnreachable();
1010+
return this;
1011+
}
10001012
}
10011013

10021014
Bucket(Storage storage, BucketInfo.BuilderImpl infoBuilder) {

google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public class BucketInfo implements Serializable {
131131
customerManagedEncryptionEnforcementConfig;
132132
private final @Nullable CustomerSuppliedEncryptionEnforcementConfig
133133
customerSuppliedEncryptionEnforcementConfig;
134+
private final Boolean isUnreachable;
134135

135136
private final transient ImmutableSet<NamedField> modifiedFields;
136137

@@ -2638,6 +2639,8 @@ public Builder setRetentionPeriodDuration(Duration retentionPeriod) {
26382639
*/
26392640
public abstract Builder setIpFilter(IpFilter ipFilter);
26402641

2642+
public abstract Builder setIsUnreachable(Boolean isUnreachable);
2643+
26412644
/** Creates a {@code BucketInfo} object. */
26422645
public abstract BucketInfo build();
26432646

@@ -2708,6 +2711,8 @@ public Builder setRetentionPeriodDuration(Duration retentionPeriod) {
27082711
abstract Builder clearCustomerManagedEncryptionEnforcementConfig();
27092712

27102713
abstract Builder clearCustomerSuppliedEncryptionEnforcementConfig();
2714+
2715+
abstract Builder clearIsUnreachable();
27112716
}
27122717

27132718
static final class BuilderImpl extends Builder {
@@ -2751,6 +2756,7 @@ static final class BuilderImpl extends Builder {
27512756
private GoogleManagedEncryptionEnforcementConfig googleManagedEncryptionEnforcementConfig;
27522757
private CustomerManagedEncryptionEnforcementConfig customerManagedEncryptionEnforcementConfig;
27532758
private CustomerSuppliedEncryptionEnforcementConfig customerSuppliedEncryptionEnforcementConfig;
2759+
private Boolean isUnreachable;
27542760
private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
27552761

27562762
BuilderImpl(String name) {
@@ -2799,6 +2805,7 @@ static final class BuilderImpl extends Builder {
27992805
bucketInfo.customerManagedEncryptionEnforcementConfig;
28002806
customerSuppliedEncryptionEnforcementConfig =
28012807
bucketInfo.customerSuppliedEncryptionEnforcementConfig;
2808+
isUnreachable = bucketInfo.isUnreachable;
28022809
}
28032810

28042811
@Override
@@ -3250,6 +3257,13 @@ public Builder setIpFilter(IpFilter ipFilter) {
32503257
return this;
32513258
}
32523259

3260+
@Override
3261+
public Builder setIsUnreachable(Boolean isUnreachable) {
3262+
Boolean tmp = firstNonNull(isUnreachable, Data.<Boolean>nullOf(Boolean.class));
3263+
this.isUnreachable = tmp;
3264+
return this;
3265+
}
3266+
32533267
@Override
32543268
public BucketInfo build() {
32553269
checkNotNull(name);
@@ -3460,6 +3474,12 @@ BuilderImpl clearCustomerSuppliedEncryptionEnforcementConfig() {
34603474
return this;
34613475
}
34623476

3477+
@Override
3478+
BuilderImpl clearIsUnreachable() {
3479+
this.isUnreachable = null;
3480+
return this;
3481+
}
3482+
34633483
private Builder clearDeleteLifecycleRules() {
34643484
if (lifecycleRules != null && !lifecycleRules.isEmpty()) {
34653485
ImmutableList<LifecycleRule> nonDeleteRules =
@@ -3513,6 +3533,7 @@ private Builder clearDeleteLifecycleRules() {
35133533
customerManagedEncryptionEnforcementConfig = builder.customerManagedEncryptionEnforcementConfig;
35143534
customerSuppliedEncryptionEnforcementConfig =
35153535
builder.customerSuppliedEncryptionEnforcementConfig;
3536+
isUnreachable = builder.isUnreachable;
35163537
modifiedFields = builder.modifiedFields.build();
35173538
}
35183539

@@ -3886,6 +3907,16 @@ public HierarchicalNamespace getHierarchicalNamespace() {
38863907
return customerSuppliedEncryptionEnforcementConfig;
38873908
}
38883909

3910+
/**
3911+
* Returns a {@code Boolean} with {@code true} if the bucket is unreachable, else {@code null}
3912+
*
3913+
* <p>A bucket may be unreachable if the region in which it resides is experiencing an outage or
3914+
* if there are other temporary access issues.
3915+
*/
3916+
public Boolean isUnreachable() {
3917+
return Data.isNull(isUnreachable) ? null : isUnreachable;
3918+
}
3919+
38893920
/** Returns a builder for the current bucket. */
38903921
public Builder toBuilder() {
38913922
return new BuilderImpl(this);
@@ -3931,7 +3962,8 @@ public int hashCode() {
39313962
ipFilter,
39323963
googleManagedEncryptionEnforcementConfig,
39333964
customerManagedEncryptionEnforcementConfig,
3934-
customerSuppliedEncryptionEnforcementConfig);
3965+
customerSuppliedEncryptionEnforcementConfig,
3966+
isUnreachable);
39353967
}
39363968

39373969
@Override
@@ -3985,7 +4017,8 @@ public boolean equals(Object o) {
39854017
that.customerManagedEncryptionEnforcementConfig)
39864018
&& Objects.equals(
39874019
customerSuppliedEncryptionEnforcementConfig,
3988-
that.customerSuppliedEncryptionEnforcementConfig);
4020+
that.customerSuppliedEncryptionEnforcementConfig)
4021+
&& Objects.equals(isUnreachable, that.isUnreachable);
39894022
}
39904023

39914024
@Override

google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.cloud.storage.Storage.BucketField.IP_FILTER;
2020
import static com.google.cloud.storage.Storage.BucketField.SOFT_DELETE_POLICY;
21+
import static com.google.cloud.storage.Utils.bucketNameCodec;
2122
import static com.google.cloud.storage.Utils.dateTimeCodec;
2223
import static com.google.cloud.storage.Utils.durationSecondsCodec;
2324
import static com.google.cloud.storage.Utils.ifNonNull;
@@ -609,7 +610,7 @@ private Bucket bucketInfoEncode(BucketInfo from) {
609610

610611
@SuppressWarnings("deprecation")
611612
private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket from) {
612-
BucketInfo.Builder to = new BucketInfo.BuilderImpl(from.getName());
613+
BucketInfo.Builder to = new BucketInfo.BuilderImpl(bucketNameCodec.decode(from.getName()));
613614
ifNonNull(from.getProjectNumber(), to::setProject);
614615
ifNonNull(from.getAcl(), toListOf(bucketAcl()::decode), to::setAcl);
615616
ifNonNull(from.getCors(), toListOf(cors()::decode), to::setCors);
@@ -674,6 +675,9 @@ private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket
674675
ifNonNull(from.getObjectRetention(), this::objectRetentionDecode, to::setObjectRetention);
675676
ifNonNull(from.getSoftDeletePolicy(), this::softDeletePolicyDecode, to::setSoftDeletePolicy);
676677
ifNonNull(from.getIpFilter(), ipFilterCodec::decode, to::setIpFilter);
678+
if (from.containsKey("isUnreachable")) {
679+
to.setIsUnreachable(Boolean.TRUE);
680+
}
677681
return to.build();
678682
}
679683

google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2476,6 +2476,11 @@ public static BucketListOption pageToken(@NonNull String pageToken) {
24762476
return new BucketListOption(UnifiedOpts.pageToken(pageToken));
24772477
}
24782478

2479+
@TransportCompatibility({Transport.HTTP})
2480+
public static BucketListOption returnPartialSuccess(boolean returnPartialSuccess) {
2481+
return new BucketListOption(UnifiedOpts.returnPartialSuccess(returnPartialSuccess));
2482+
}
2483+
24792484
/**
24802485
* Returns an option to set a prefix to filter results to buckets whose names begin with this
24812486
* prefix.

google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ static PageToken pageToken(@NonNull String pageToken) {
463463
return new PageToken(pageToken);
464464
}
465465

466+
static ReturnPartialSuccess returnPartialSuccess(boolean returnPartialSuccess) {
467+
return new ReturnPartialSuccess(returnPartialSuccess);
468+
}
469+
466470
static PredefinedAcl predefinedAcl(Storage.@NonNull PredefinedAcl predefinedAcl) {
467471
requireNonNull(predefinedAcl, "predefinedAcl must be non null");
468472
return new PredefinedAcl(predefinedAcl.getEntry());
@@ -1639,6 +1643,19 @@ public Mapper<ListObjectsRequest.Builder> listObjects() {
16391643
}
16401644
}
16411645

1646+
static final class ReturnPartialSuccess extends RpcOptVal<Boolean> implements BucketListOpt {
1647+
private static final long serialVersionUID = -1370658416509499277L;
1648+
1649+
private ReturnPartialSuccess(boolean val) {
1650+
super(StorageRpc.Option.RETURN_PARTIAL_SUCCESS, val);
1651+
}
1652+
1653+
@Override
1654+
public Mapper<ListBucketsRequest.Builder> listBuckets() {
1655+
return b -> b.setReturnPartialSuccess(val);
1656+
}
1657+
}
1658+
16421659
static final class PredefinedAcl extends RpcOptVal<String>
16431660
implements BucketTargetOpt, ObjectTargetOpt {
16441661
private static final long serialVersionUID = -1743736785228368741L;

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,11 +461,18 @@ public Tuple<String, Iterable<Bucket>> list(Map<Option, ?> options) {
461461
.setPrefix(Option.PREFIX.getString(options))
462462
.setMaxResults(Option.MAX_RESULTS.getLong(options))
463463
.setPageToken(Option.PAGE_TOKEN.getString(options))
464+
.setReturnPartialSuccess(Option.RETURN_PARTIAL_SUCCESS.getBoolean(options))
464465
.setFields(Option.FIELDS.getString(options))
465466
.setUserProject(Option.USER_PROJECT.getString(options));
466467
setExtraHeaders(list, options);
467-
com.google.api.services.storage.model.Buckets buckets = list.execute();
468-
return Tuple.<String, Iterable<Bucket>>of(buckets.getNextPageToken(), buckets.getItems());
468+
com.google.api.services.storage.model.Buckets bucketList = list.execute();
469+
Iterable<Bucket> buckets =
470+
Iterables.concat(
471+
firstNonNull(bucketList.getItems(), ImmutableList.<Bucket>of()),
472+
bucketList.getUnreachable() != null
473+
? Lists.transform(bucketList.getUnreachable(), createUnreachableBucket())
474+
: ImmutableList.<Bucket>of());
475+
return Tuple.<String, Iterable<Bucket>>of(bucketList.getNextPageToken(), buckets);
469476
} catch (IOException ex) {
470477
span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
471478
throw translate(ex);
@@ -530,6 +537,10 @@ private static String detectContentType(StorageObject object, Map<Option, ?> opt
530537
return firstNonNull(contentType, "application/octet-stream");
531538
}
532539

540+
private static Function<String, Bucket> createUnreachableBucket() {
541+
return bucketName -> new Bucket().setName(bucketName).set("isUnreachable", "true");
542+
}
543+
533544
private static Function<String, StorageObject> objectFromPrefix(final String bucket) {
534545
return new Function<String, StorageObject>() {
535546
@Override

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ enum Option {
5858
PROJECTION("projection"),
5959
MAX_RESULTS("maxResults"),
6060
PAGE_TOKEN("pageToken"),
61+
RETURN_PARTIAL_SUCCESS("returnPartialSuccess"),
6162
DELIMITER("delimiter"),
6263
START_OFF_SET("startOffset"),
6364
END_OFF_SET("endOffset"),

google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ public class StorageImplMockitoTest {
200200
Storage.BucketListOption.fields();
201201
private static final Map<StorageRpc.Option, ?> BUCKET_LIST_OPTIONS =
202202
ImmutableMap.of(StorageRpc.Option.MAX_RESULTS, PAGE_SIZE, StorageRpc.Option.PREFIX, "prefix");
203+
private static final Map<StorageRpc.Option, ?> BUCKET_LIST_PARTIAL_SUCCESS_OPTION =
204+
ImmutableMap.of(StorageRpc.Option.RETURN_PARTIAL_SUCCESS, true);
203205

204206
// Blob list options
205207
private static final Storage.BlobListOption BLOB_LIST_PAGE_SIZE =
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage.it;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.google.api.gax.paging.Page;
22+
import com.google.cloud.storage.Bucket;
23+
import com.google.cloud.storage.BucketInfo;
24+
import com.google.cloud.storage.Storage;
25+
import com.google.cloud.storage.Storage.BucketListOption;
26+
import com.google.cloud.storage.TransportCompatibility.Transport;
27+
import com.google.cloud.storage.it.runner.StorageITRunner;
28+
import com.google.cloud.storage.it.runner.annotations.Backend;
29+
import com.google.cloud.storage.it.runner.annotations.BucketFixture;
30+
import com.google.cloud.storage.it.runner.annotations.BucketType;
31+
import com.google.cloud.storage.it.runner.annotations.CrossRun;
32+
import com.google.cloud.storage.it.runner.annotations.Inject;
33+
import com.google.cloud.storage.it.runner.registry.Generator;
34+
import com.google.common.collect.ImmutableMap;
35+
import java.util.Map;
36+
import java.util.stream.Collectors;
37+
import org.junit.Test;
38+
import org.junit.runner.RunWith;
39+
40+
@RunWith(StorageITRunner.class)
41+
@CrossRun(
42+
backends = {Backend.TEST_BENCH},
43+
transports = {Transport.HTTP})
44+
public class ITListBucketTest {
45+
@Inject public Storage storage;
46+
47+
@Inject public BucketInfo defaultBucket;
48+
49+
@Inject
50+
@BucketFixture(BucketType.HNS)
51+
public BucketInfo hnsBucket;
52+
53+
@Inject public Generator generator;
54+
55+
private static final String UNREACHABLE_BUCKET_SUFFIX = ".unreachable";
56+
57+
@Test
58+
public void testListBucketWithPartialSuccess() throws Exception {
59+
doTest(Reachability.Unreachable, BucketListOption.returnPartialSuccess(true));
60+
}
61+
62+
@Test
63+
public void testListBucketWithoutPartialSuccess() throws Exception {
64+
doTest(Reachability.Reachable);
65+
}
66+
67+
private void doTest(
68+
Reachability expectedReachabilityOfUnreachableBucket, BucketListOption... bucketListOption)
69+
throws Exception {
70+
// TESTBENCH considers a bucket to be unreachable if the bucket name contains "unreachable"
71+
String name = generator.randomBucketName() + UNREACHABLE_BUCKET_SUFFIX;
72+
BucketInfo info = BucketInfo.of(name);
73+
try (TemporaryBucket tmpBucket =
74+
TemporaryBucket.newBuilder().setBucketInfo(info).setStorage(storage).build()) {
75+
Map<String, Reachability> expected =
76+
ImmutableMap.of(
77+
defaultBucket.getName(), Reachability.Reachable,
78+
hnsBucket.getName(), Reachability.Reachable,
79+
tmpBucket.getBucket().getName(), expectedReachabilityOfUnreachableBucket);
80+
81+
Page<Bucket> page = storage.list(bucketListOption);
82+
83+
Map<String, Reachability> actual =
84+
page.streamAll().collect(Collectors.toMap(BucketInfo::getName, Reachability::forBucket));
85+
86+
assertThat(actual).containsAtLeastEntriesIn(expected);
87+
}
88+
}
89+
90+
private enum Reachability {
91+
Reachable,
92+
Unreachable;
93+
94+
static Reachability forBucket(BucketInfo b) {
95+
if (b.isUnreachable() != null && b.isUnreachable()) {
96+
return Unreachable;
97+
} else {
98+
return Reachable;
99+
}
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)