Skip to content

Commit 8880d94

Browse files
authored
feat: add new JournalingBlobWriteSessionConfig usable with gRPC transport (#2194)
### Overview New BlobWriteSessionConfig that will use disk to "Journal" data before transmitting to GCS. By journaling the data to disk we can rewind to an arbitrary offset in the case of failure, while being optimistic and keeping the stream open to gcs. General flow of data into this new WritableByteChannel: 1. #write(ByteBuffer) 2. Write contents of ByteBuffer to recovery file on disk 3. force a flush to disk 4. transmit contents of ByteBuffer to GCS leaving stream open 5. return If the stream to gcs is interrupted with a retriable error 1. query the offset that was successfully committed to GCS 2. rewind our transmit context to the offset 3. Open the recovery file at offset from query 4. stream contents from recovery file to GCS leaving stream open 5. Once recovery file contents are transmitted to GCS return ### Benchmark results #### Setup 1. c2d-standard-32 in us-east1 * with 4 local nvme ssd used for the recovery files * default NIC, premium network tier * debian 11 image 2. regional bucket in us-east1 #### Workload generate a random file with size `1GiB..2GiB` upload random file to GCS using each of the following configurations: 1. DefaultBlobWriteSessionConfig with 16MiB chunk size 2. DefaultBlobWriteSessionConfig with 64MiB chunk size 3. BufferToDiskThenUpload with the following directories as buffer locations: /mnt/ssd1:/mnt/ssd2:/mnt/ssd3:/mnt/ssd4 4. JournalingBlobWriteSessionConfig with the following directories as journal locations: /mnt/ssd1:/mnt/ssd2:/mnt/ssd3:/mnt/ssd4 Run across `{1,2,4,8,16,32}` concurrent threads to evaluate contention and horizontal scaling. #### Collected metrics 1. Generate random file 2. record begin instant 3. create new BlobWriteSession 4. BlobWriteSession#open() 5. Copy all bytes from random file to WritableByteChannel from 4 6. close the WritableByteChannel from 4 7. record end instant 8. report `objectSize,UploadStrategy,elapsedTimeUs,Status,ThreadCount` #### Results summary Throuhgput in MiB/s, grouped by ThreadCount and UploadStrategy ``` count mean std min 50% 75% 90% 99% max ThreadCount UploadStrategy 1 PUT 16MiB 4341.0 79.941 8.752 21.599 80.218 85.628 90.710 99.635 106.627 PUT 64MiB 4341.0 100.410 11.555 20.490 100.022 108.208 115.214 128.251 139.710 BtDtU 4341.0 104.728 22.527 39.265 110.374 122.335 130.899 146.897 158.975 journaling 4341.0 125.820 31.332 45.502 133.590 149.027 161.899 188.716 201.938 2 PUT 16MiB 4237.0 80.000 8.525 15.814 80.693 85.651 90.241 97.958 106.677 PUT 64MiB 4237.0 101.062 11.030 55.813 101.049 108.007 115.114 127.299 135.149 BtDtU 4237.0 104.236 21.031 5.602 109.382 120.411 128.532 143.113 162.146 journaling 4237.0 125.010 29.761 43.207 131.827 147.362 159.425 182.441 209.000 4 PUT 16MiB 4411.0 79.708 8.357 40.691 80.600 85.567 89.586 95.533 103.506 PUT 64MiB 4411.0 100.536 9.947 58.084 100.846 107.209 113.144 122.172 131.974 BtDtU 4411.0 103.421 21.314 36.401 108.778 119.887 128.550 144.903 158.948 journaling 4411.0 123.705 30.707 40.082 130.553 147.581 159.995 186.684 222.646 8 PUT 16MiB 4260.0 79.314 8.393 7.148 80.153 85.175 89.319 95.475 100.757 PUT 64MiB 4260.0 99.913 10.438 60.685 100.450 107.144 112.551 122.409 132.130 BtDtU 4260.0 102.472 21.228 32.552 108.226 119.072 126.700 142.831 155.628 journaling 4260.0 122.931 30.261 42.747 130.306 146.098 158.005 184.798 203.696 16 PUT 16MiB 4473.0 77.735 8.091 24.149 78.483 83.123 87.092 95.740 106.176 PUT 64MiB 4473.0 97.690 9.987 45.342 97.768 103.996 109.807 122.202 140.906 BtDtU 4473.0 99.314 21.090 39.412 104.270 116.041 124.532 139.305 148.162 journaling 4473.0 118.956 30.486 44.253 122.585 143.344 156.484 182.211 200.777 32 PUT 16MiB 4024.0 72.923 8.045 20.205 73.601 78.575 82.341 88.970 100.665 PUT 64MiB 4024.0 93.151 10.030 20.913 93.506 99.748 105.297 116.163 128.284 BtDtU 4024.0 89.134 18.995 35.633 91.033 103.698 112.994 131.555 146.751 journaling 4024.0 104.557 30.965 11.785 98.794 129.923 146.747 174.618 200.303 ```
1 parent 1fa49db commit 8880d94

19 files changed

+1264
-24
lines changed

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

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,115 @@
2121
import com.google.cloud.storage.Storage.BlobWriteOption;
2222
import com.google.common.collect.ImmutableList;
2323
import java.io.IOException;
24+
import java.nio.channels.WritableByteChannel;
2425
import java.nio.file.Path;
2526
import java.nio.file.Paths;
2627
import java.util.Collection;
2728

2829
/**
2930
* Factory class to select and construct {@link BlobWriteSessionConfig}s.
3031
*
32+
* <p>There are several strategies which can be used to upload a {@link Blob} to Google Cloud
33+
* Storage. This class provides factories which allow you to select the appropriate strategy for
34+
* your workload.
35+
*
36+
* <table>
37+
* <caption>Comparison of Strategies</caption>
38+
* <tr>
39+
* <th>Strategy</th>
40+
* <th>Factory Method(s)</th>
41+
* <th>Description</th>
42+
* <th>Retry Support</th>
43+
* <th>Transports Supported</th>
44+
* <th>Cloud Storage API used</th>
45+
* <th>Considerations</th>
46+
* </tr>
47+
* <tr>
48+
* <td>Default (Chunk based upload)</td>
49+
* <td>{@link #getDefault()}</td>
50+
* <td>
51+
* Buffer up to a configurable amount of bytes in memory, write to Cloud Storage when
52+
* full or close. Buffer size is configurable via
53+
* {@link DefaultBlobWriteSessionConfig#withChunkSize(int)}
54+
* </td>
55+
* <td>
56+
* Each chunk is retried up to the limitations specified in
57+
* {@link StorageOptions#getRetrySettings()}
58+
* </td>
59+
* <td>gRPC</td>
60+
* <td><a href="https://cloud.google.com/storage/docs/resumable-uploads">Resumable Upload</a></td>
61+
* <td>The network will only be used for the following operations:
62+
* <ol>
63+
* <li>Creating the Resumable Upload Session</li>
64+
* <li>Transmitting zero or more incremental chunks</li>
65+
* <li>Transmitting the final chunk and finalizing the Resumable Upload Session</li>
66+
* <li>
67+
* If any of the above are interrupted with a retryable error, the Resumable Upload Session
68+
* will be queried to reconcile client side state with Cloud Storage
69+
* </li>
70+
* </ol>
71+
* </td>
72+
* </tr>
73+
* <tr>
74+
* <td>Buffer to disk then upload</td>
75+
* <td>
76+
* <ul>
77+
* <li>{@link #bufferToDiskThenUpload(Path)}</li>
78+
* <li>{@link #bufferToDiskThenUpload(Collection) bufferToDiskThenUpload(Collection&lt;Path>)}</li>
79+
* <li>{@link #bufferToTempDirThenUpload()}</li>
80+
* </ul>
81+
* </td>
82+
* <td>
83+
* Buffer bytes to a temporary file on disk. On {@link WritableByteChannel#close() close()}
84+
* upload the entire files contents to Cloud Storage. Delete the temporary file.
85+
* </td>
86+
* <td>
87+
* Upload the file in the fewest number of RPC possible retrying within the limitations
88+
* specified in {@link StorageOptions#getRetrySettings()}
89+
* </td>
90+
* <td>gRPC</td>
91+
* <td><a href="https://cloud.google.com/storage/docs/resumable-uploads">Resumable Upload</a></td>
92+
* <td>
93+
* <ol>
94+
* <li>A Resumable Upload Session will be used to upload the file on disk.</li>
95+
* <li>
96+
* If the upload is interrupted with a retryable error, the Resumable Upload Session will
97+
* be queried to restart the upload from Cloud Storage's last received byte
98+
* </li>
99+
* </ol>
100+
* </td>
101+
* </tr>
102+
* <tr>
103+
* <td>Journal to disk while uploading</td>
104+
* <td>{@link #journaling(Collection) journaling(Collection&lt;Path>)}</td>
105+
* <td>
106+
* Create a Resumable Upload Session, before transmitting bytes to Cloud Storage write
107+
* to a recovery file on disk. If the stream to Cloud Storage is interrupted with a
108+
* retryable error query the offset of the Resumable Upload Session, then open the recovery
109+
* file from the offset and transmit the bytes to Cloud Storage.
110+
* </td>
111+
* <td>gRPC</td>
112+
* <td><a href="https://cloud.google.com/storage/docs/resumable-uploads">Resumable Upload</a></td>
113+
* <td>
114+
* <ol>
115+
* <li>
116+
* The stream to Cloud Storage will be held open until a) the write is complete
117+
* b) the stream is interrupted
118+
* </li>
119+
* <li>
120+
* Because the bytes are journaled to disk, the upload to Cloud Storage can only
121+
* be as fast as the disk.
122+
* </li>
123+
* <li>
124+
* The use of <a href="https://cloud.google.com/compute/docs/disks/local-ssd#nvme">Compute
125+
* Engine Local NVMe SSD</a> is strongly encouraged compared to Compute Engine Persistent
126+
* Disk.
127+
* </li>
128+
* </ol>
129+
* </td>
130+
* </tr>
131+
* </table>
132+
*
31133
* @see BlobWriteSessionConfig
32134
* @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
33135
* @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
@@ -56,11 +158,11 @@ public static DefaultBlobWriteSessionConfig getDefault() {
56158
* Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object
57159
* to a temporary file under {@code java.io.tmpdir}.
58160
*
59-
* <p>Once the file on disk is closed, the entire file will then be uploaded to Google Cloud
60-
* Storage.
161+
* <p>Once the file on disk is closed, the entire file will then be uploaded to Cloud Storage.
61162
*
62163
* @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
63164
* @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
165+
* @since 2.26.0 This new api is in preview and is subject to breaking changes.
64166
*/
65167
@BetaApi
66168
public static BlobWriteSessionConfig bufferToTempDirThenUpload() throws IOException {
@@ -72,11 +174,11 @@ public static BlobWriteSessionConfig bufferToTempDirThenUpload() throws IOExcept
72174
* Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object
73175
* to a temporary file under the specified {@code path}.
74176
*
75-
* <p>Once the file on disk is closed, the entire file will then be uploaded to Google Cloud
76-
* Storage.
177+
* <p>Once the file on disk is closed, the entire file will then be uploaded to Cloud Storage.
77178
*
78179
* @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
79180
* @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
181+
* @since 2.26.0 This new api is in preview and is subject to breaking changes.
80182
*/
81183
@BetaApi
82184
public static BufferToDiskThenUpload bufferToDiskThenUpload(Path path) throws IOException {
@@ -87,18 +189,34 @@ public static BufferToDiskThenUpload bufferToDiskThenUpload(Path path) throws IO
87189
* Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object
88190
* to a temporary file under one of the specified {@code paths}.
89191
*
90-
* <p>Once the file on disk is closed, the entire file will then be uploaded to Google Cloud
91-
* Storage.
192+
* <p>Once the file on disk is closed, the entire file will then be uploaded to Cloud Storage.
92193
*
93194
* <p>The specifics of how the work is spread across multiple paths is undefined and subject to
94195
* change.
95196
*
96197
* @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
97198
* @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
199+
* @since 2.26.0 This new api is in preview and is subject to breaking changes.
98200
*/
99201
@BetaApi
100202
public static BufferToDiskThenUpload bufferToDiskThenUpload(Collection<Path> paths)
101203
throws IOException {
102204
return new BufferToDiskThenUpload(ImmutableList.copyOf(paths), false);
103205
}
206+
207+
/**
208+
* Create a new {@link BlobWriteSessionConfig} which will journal writes to a temporary file under
209+
* one of the specified {@code paths} before transmitting the bytes to Cloud Storage.
210+
*
211+
* <p>The specifics of how the work is spread across multiple paths is undefined and subject to
212+
* change.
213+
*
214+
* @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...)
215+
* @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig)
216+
* @since 2.27.0 This new api is in preview and is subject to breaking changes.
217+
*/
218+
@BetaApi
219+
public static JournalingBlobWriteSessionConfig journaling(Collection<Path> paths) {
220+
return new JournalingBlobWriteSessionConfig(ImmutableList.copyOf(paths), false);
221+
}
104222
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.core.InternalApi;
2323
import com.google.api.core.SettableApiFuture;
2424
import com.google.cloud.storage.Conversions.Decoder;
25+
import com.google.cloud.storage.RecoveryFileManager.RecoveryVolumeSinkFactory;
2526
import com.google.cloud.storage.Storage.BlobWriteOption;
2627
import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt;
2728
import com.google.cloud.storage.UnifiedOpts.Opts;
@@ -96,8 +97,7 @@ WriterFactory createFactory(Clock clock) throws IOException {
9697
return new Factory(recoveryFileManager, clock, gcs);
9798
}
9899

99-
private RecoveryFileManager.RecoverVolumeSinkFactory getRecoverVolumeSinkFactory(
100-
Clock clock, Duration window) {
100+
private RecoveryVolumeSinkFactory getRecoverVolumeSinkFactory(Clock clock, Duration window) {
101101
return path -> {
102102
ThroughputSink windowed = ThroughputSink.windowed(ThroughputMovingWindow.of(window), clock);
103103
if (includeLoggingSink) {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import java.io.IOException;
1920
import java.nio.Buffer;
2021
import java.nio.ByteBuffer;
22+
import java.nio.channels.ReadableByteChannel;
2123
import java.util.function.Consumer;
2224

2325
/**
@@ -137,4 +139,19 @@ static int alignSize(int size, int alignmentMultiple) {
137139
} // else size is already aligned
138140
return alignedSize;
139141
}
142+
143+
static int fillFrom(ByteBuffer buf, ReadableByteChannel c) throws IOException {
144+
int total = 0;
145+
while (buf.hasRemaining()) {
146+
int read = c.read(buf);
147+
if (read != -1) {
148+
total += read;
149+
} else if (total == 0) {
150+
return -1;
151+
} else {
152+
break;
153+
}
154+
}
155+
return total;
156+
}
140157
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ Hasher getHasher() {
5050
return hasher;
5151
}
5252

53+
ChunkSegment[] segmentBuffer(ByteBuffer bb) {
54+
return segmentBuffers(new ByteBuffer[] {bb}, 0, 1);
55+
}
56+
5357
/**
5458
* Given {@code bbs}, yield N segments, where each segment is at most {@code maxSegmentSize}
5559
* bytes.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ public int getChunkSize() {
7979
*
8080
* <p><i>Default:</i> {@code 16777216 (16 MiB)}
8181
*
82-
* @param chunkSize The number of bytes each chunk should be. Must be >= {@code 262144 (256 KiB)}
82+
* @param chunkSize The number of bytes each chunk should be. Must be &gt;= {@code 262144 (256
83+
* KiB)}
8384
* @return The new instance
8485
* @see #getChunkSize()
8586
* @since 2.26.0 This new api is in preview and is subject to breaking changes.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ public int write(ByteBuffer src) throws IOException {
145145
ByteBuffer slice = src.slice();
146146
Buffers.limit(slice, bufferRemaining);
147147
int write = channel.write(slice);
148-
Buffers.position(src, srcPosition + write);
148+
int newPosition = srcPosition + write;
149+
Buffers.position(src, newPosition);
149150
bytesConsumed += write;
150151
}
151152
}

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
import com.google.api.core.SettableApiFuture;
2525
import com.google.api.gax.retrying.ResultRetryAlgorithm;
2626
import com.google.api.gax.rpc.ClientStreamingCallable;
27+
import com.google.api.gax.rpc.UnaryCallable;
2728
import com.google.cloud.storage.ChannelSession.BufferedWriteSession;
2829
import com.google.cloud.storage.ChannelSession.UnbufferedWriteSession;
2930
import com.google.cloud.storage.Retrying.RetryingDependencies;
3031
import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
3132
import com.google.cloud.storage.WriteCtx.WriteObjectRequestBuilderFactory;
3233
import com.google.cloud.storage.WriteFlushStrategy.FlusherFactory;
34+
import com.google.storage.v2.QueryWriteStatusRequest;
35+
import com.google.storage.v2.QueryWriteStatusResponse;
3336
import com.google.storage.v2.ServiceConstants.Values;
3437
import com.google.storage.v2.WriteObjectRequest;
3538
import com.google.storage.v2.WriteObjectResponse;
@@ -106,6 +109,10 @@ ResumableUploadBuilder resumable() {
106109
return new ResumableUploadBuilder();
107110
}
108111

112+
JournalingResumableUploadBuilder journaling() {
113+
return new JournalingResumableUploadBuilder();
114+
}
115+
109116
/**
110117
* When constructing any of our channel sessions, there is always a {@link
111118
* GapicUnbufferedWritableByteChannel} at the bottom of it. This method creates a BiFunction which
@@ -332,4 +339,102 @@ BufferedWritableByteChannelSession<WriteObjectResponse> build() {
332339
}
333340
}
334341
}
342+
343+
final class JournalingResumableUploadBuilder {
344+
345+
private RetryingDependencies deps;
346+
private ResultRetryAlgorithm<?> alg;
347+
private BufferHandle bufferHandle;
348+
private BufferHandle recoveryBuffer;
349+
private RecoveryFile recoveryFile;
350+
private UnaryCallable<QueryWriteStatusRequest, QueryWriteStatusResponse> query;
351+
352+
JournalingResumableUploadBuilder() {
353+
this.deps = RetryingDependencies.attemptOnce();
354+
this.alg = Retrying.neverRetry();
355+
}
356+
357+
JournalingResumableUploadBuilder withRetryConfig(
358+
RetryingDependencies deps,
359+
ResultRetryAlgorithm<?> alg,
360+
UnaryCallable<QueryWriteStatusRequest, QueryWriteStatusResponse> query) {
361+
this.deps = requireNonNull(deps, "deps must be non null");
362+
this.alg = requireNonNull(alg, "alg must be non null");
363+
this.query = requireNonNull(query, "query must be non null");
364+
return this;
365+
}
366+
367+
JournalingResumableUploadBuilder withBuffer(BufferHandle bufferHandle) {
368+
this.bufferHandle = requireNonNull(bufferHandle, "bufferHandle must be non null");
369+
return this;
370+
}
371+
372+
JournalingResumableUploadBuilder withRecoveryBuffer(BufferHandle bufferHandle) {
373+
this.recoveryBuffer = requireNonNull(bufferHandle, "bufferHandle must be non null");
374+
return this;
375+
}
376+
377+
JournalingResumableUploadBuilder withRecoveryFile(RecoveryFile recoveryFile) {
378+
this.recoveryFile = requireNonNull(recoveryFile, "recoveryFile must be non null");
379+
return this;
380+
}
381+
382+
/**
383+
* Set the Future which will contain the ResumableWrite information necessary to open the Write
384+
* stream.
385+
*/
386+
BuildableJournalingResumableUploadBuilder setStartAsync(
387+
ApiFuture<WriteCtx<ResumableWrite>> start) {
388+
requireNonNull(start, "start must be non null");
389+
return new BuildableJournalingResumableUploadBuilder(start);
390+
}
391+
392+
final class BuildableJournalingResumableUploadBuilder {
393+
private final ApiFuture<WriteCtx<ResumableWrite>> start;
394+
395+
private BuildableJournalingResumableUploadBuilder(ApiFuture<WriteCtx<ResumableWrite>> start) {
396+
this.start = start;
397+
}
398+
399+
BufferedWritableByteChannelSession<WriteObjectResponse> build() {
400+
return new BufferedWriteSession<>(
401+
requireNonNull(start, "start must be non null"),
402+
bindFunction()
403+
.andThen(c -> new DefaultBufferedWritableByteChannel(bufferHandle, c))
404+
.andThen(StorageByteChannels.writable()::createSynchronized));
405+
}
406+
407+
private BiFunction<
408+
WriteCtx<ResumableWrite>,
409+
SettableApiFuture<WriteObjectResponse>,
410+
UnbufferedWritableByteChannel>
411+
bindFunction() {
412+
// it is theoretically possible that the setter methods for the following variables could
413+
// be called again between when this method is invoked and the resulting function is
414+
// invoked.
415+
// To ensure we are using the specified values at the point in time they are bound to the
416+
// function read them into local variables which will be closed over rather than the class
417+
// fields.
418+
RetryingDependencies deps = JournalingResumableUploadBuilder.this.deps;
419+
ResultRetryAlgorithm<?> alg = JournalingResumableUploadBuilder.this.alg;
420+
BufferHandle recoveryBuffer = JournalingResumableUploadBuilder.this.recoveryBuffer;
421+
RecoveryFile recoveryFile = JournalingResumableUploadBuilder.this.recoveryFile;
422+
UnaryCallable<QueryWriteStatusRequest, QueryWriteStatusResponse> query =
423+
JournalingResumableUploadBuilder.this.query;
424+
ByteStringStrategy boundStrategy = byteStringStrategy;
425+
Hasher boundHasher = hasher;
426+
return (writeCtx, resultFuture) ->
427+
new SyncAndUploadUnbufferedWritableByteChannel(
428+
write,
429+
query,
430+
resultFuture,
431+
new ChunkSegmenter(boundHasher, boundStrategy, Values.MAX_WRITE_CHUNK_BYTES_VALUE),
432+
deps,
433+
alg,
434+
writeCtx,
435+
recoveryFile,
436+
recoveryBuffer);
437+
}
438+
}
439+
}
335440
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ ReadObjectRequest getReadObjectRequest(BlobId blob, Opts<ObjectSourceOpt> opts)
17681768
return opts.readObjectRequest().apply(builder).build();
17691769
}
17701770

1771-
private WriteObjectRequest getWriteObjectRequest(BlobInfo info, Opts<ObjectTargetOpt> opts) {
1771+
WriteObjectRequest getWriteObjectRequest(BlobInfo info, Opts<ObjectTargetOpt> opts) {
17721772
Object object = codecs.blobInfo().encode(info);
17731773
Object.Builder objectBuilder =
17741774
object

0 commit comments

Comments
 (0)