Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(1.113.14-sp): backport critical bug fixes #993

Merged
merged 5 commits into from Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions google-cloud-storage/clirr-ignored-differences.xml
Expand Up @@ -31,4 +31,9 @@
<method>long getCurrentUploadOffset(java.lang.String)</method>
<differenceType>7012</differenceType>
</difference>
<difference>
<className>com/google/cloud/storage/spi/v1/StorageRpc</className>
<method>com.google.api.services.storage.model.StorageObject queryCompletedResumableUpload(java.lang.String, long)</method>
<differenceType>7012</differenceType>
</difference>
</differences>
Expand Up @@ -25,7 +25,7 @@
import com.google.cloud.RetryHelper;
import com.google.cloud.WriteChannel;
import com.google.cloud.storage.spi.v1.StorageRpc;
import com.google.common.collect.Maps;
import java.math.BigInteger;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.Callable;
Expand Down Expand Up @@ -77,20 +77,52 @@ private long getRemotePosition() {
return getOptions().getStorageRpcV1().getCurrentUploadOffset(getUploadId());
}

private StorageObject getRemoteStorageObject() {
return getOptions()
.getStorageRpcV1()
.get(getEntity().toPb(), Maps.newEnumMap(StorageRpc.Option.class));
private static StorageException unrecoverableState(
String uploadId,
int chunkOffset,
int chunkLength,
long localPosition,
long remotePosition,
boolean last) {
return unrecoverableState(
uploadId,
chunkOffset,
chunkLength,
localPosition,
remotePosition,
last,
"Unable to recover in upload.\nThis may be a symptom of multiple clients uploading to the same upload session.");
}

private static StorageException errorResolvingMetadataLastChunk(
String uploadId,
int chunkOffset,
int chunkLength,
long localPosition,
long remotePosition,
boolean last) {
return unrecoverableState(
uploadId,
chunkOffset,
chunkLength,
localPosition,
remotePosition,
last,
"Unable to load object metadata to determine if last chunk was successfully written");
}

private StorageException unrecoverableState(
int chunkOffset, int chunkLength, long localPosition, long remotePosition, boolean last) {
private static StorageException unrecoverableState(
String uploadId,
int chunkOffset,
int chunkLength,
long localPosition,
long remotePosition,
boolean last,
String message) {
StringBuilder sb = new StringBuilder();
sb.append("Unable to recover in upload.\n");
sb.append(
"This may be a symptom of multiple clients uploading to the same upload session.\n\n");
sb.append(message).append("\n\n");
sb.append("For debugging purposes:\n");
sb.append("uploadId: ").append(getUploadId()).append('\n');
sb.append("uploadId: ").append(uploadId).append('\n');
sb.append("chunkOffset: ").append(chunkOffset).append('\n');
sb.append("chunkLength: ").append(chunkLength).append('\n');
sb.append("localOffset: ").append(localPosition).append('\n');
Expand Down Expand Up @@ -162,7 +194,7 @@ public void run() {
// Get remote offset from API
final long localPosition = getPosition();
// For each request it should be possible to retry from its location in this code
final long remotePosition = isRetrying() ? getRemotePosition() : getPosition();
final long remotePosition = isRetrying() ? getRemotePosition() : localPosition;
final int chunkOffset = (int) (remotePosition - localPosition);
final int chunkLength = length - chunkOffset;
final boolean uploadAlreadyComplete = remotePosition == -1;
Expand All @@ -173,13 +205,45 @@ public void run() {
if (uploadAlreadyComplete && lastChunk) {
// Case 6
// Request object metadata if not available
long totalBytes = getPosition() + length;
if (storageObject == null) {
storageObject = getRemoteStorageObject();
storageObject =
getOptions()
.getStorageRpcV1()
.queryCompletedResumableUpload(getUploadId(), totalBytes);
}
// the following checks are defined here explicitly to provide a more
// informative if either storageObject is unable to be resolved or it's size is
// unable to be determined. This scenario is a very rare case of failure that
// can arise when packets are lost.
if (storageObject == null) {
throw errorResolvingMetadataLastChunk(
getUploadId(),
chunkOffset,
chunkLength,
localPosition,
remotePosition,
lastChunk);
}
// Verify that with the final chunk we match the blob length
if (storageObject.getSize().longValue() != getPosition() + length) {
BigInteger size = storageObject.getSize();
if (size == null) {
throw errorResolvingMetadataLastChunk(
getUploadId(),
chunkOffset,
chunkLength,
localPosition,
remotePosition,
lastChunk);
}
if (size.longValue() != totalBytes) {
throw unrecoverableState(
chunkOffset, chunkLength, localPosition, remotePosition, lastChunk);
getUploadId(),
chunkOffset,
chunkLength,
localPosition,
remotePosition,
lastChunk);
}
retrying = false;
} else if (uploadAlreadyComplete && !lastChunk && !checkingForLastChunk) {
Expand All @@ -201,7 +265,12 @@ public void run() {
} else {
// Case 4 && Case 8 && Case 9
throw unrecoverableState(
chunkOffset, chunkLength, localPosition, remotePosition, lastChunk);
getUploadId(),
chunkOffset,
chunkLength,
localPosition,
remotePosition,
lastChunk);
}
}
}),
Expand Down
Expand Up @@ -43,11 +43,13 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;

/**
* Google Storage bucket metadata;
Expand Down Expand Up @@ -101,6 +103,8 @@ public com.google.api.services.storage.model.Bucket apply(BucketInfo bucketInfo)
private final String locationType;
private final Logging logging;

private static final Logger log = Logger.getLogger(BucketInfo.class.getName());

/**
* The Bucket's IAM Configuration.
*
Expand Down Expand Up @@ -356,9 +360,11 @@ public LifecycleRule(LifecycleAction action, LifecycleCondition condition) {
&& condition.getNoncurrentTimeBefore() == null
&& condition.getCustomTimeBefore() == null
&& condition.getDaysSinceCustomTime() == null) {
throw new IllegalArgumentException(
"You must specify at least one condition to use object lifecycle "
+ "management. Please see https://cloud.google.com/storage/docs/lifecycle for details.");
log.warning(
"Creating a lifecycle condition with no supported conditions:\n"
+ this
+ "\nAttempting to update with this rule may cause errors. Please update "
+ " to the latest version of google-cloud-storage");
}

this.lifecycleAction = action;
Expand Down Expand Up @@ -1833,33 +1839,52 @@ public ObjectAccessControl apply(Acl acl) {
website.setNotFoundPage(notFoundPage);
bucketPb.setWebsite(website);
}
Set<Rule> rules = new HashSet<>();
if (deleteRules != null) {
rules.addAll(
transform(
deleteRules,
new Function<DeleteRule, Rule>() {
@Override
public Rule apply(DeleteRule deleteRule) {
return deleteRule.toPb();
}
}));
}
if (lifecycleRules != null) {
rules.addAll(
transform(
lifecycleRules,
new Function<LifecycleRule, Rule>() {
@Override
public Rule apply(LifecycleRule lifecycleRule) {
return lifecycleRule.toPb();
}
}));
}

if (rules != null) {
if (deleteRules != null || lifecycleRules != null) {
Lifecycle lifecycle = new Lifecycle();
lifecycle.setRule(ImmutableList.copyOf(rules));

// Here we determine if we need to "clear" any defined Lifecycle rules by explicitly setting
// the Rule list of lifecycle to the empty list.
// In order for us to clear the rules, one of the three following must be true:
// 1. deleteRules is null while lifecycleRules is non-null and empty
// 2. lifecycleRules is null while deleteRules is non-null and empty
// 3. lifecycleRules is non-null and empty while deleteRules is non-null and empty
// If none of the above three is true, we will interpret as the Lifecycle rules being
// updated to the defined set of DeleteRule and LifecycleRule.
if ((deleteRules == null && lifecycleRules.isEmpty())
|| (lifecycleRules == null && deleteRules.isEmpty())
|| (deleteRules != null && deleteRules.isEmpty() && lifecycleRules.isEmpty())) {
lifecycle.setRule(Collections.<Rule>emptyList());
} else {
Set<Rule> rules = new HashSet<>();
if (deleteRules != null) {
rules.addAll(
transform(
deleteRules,
new Function<DeleteRule, Rule>() {
@Override
public Rule apply(DeleteRule deleteRule) {
return deleteRule.toPb();
}
}));
}
if (lifecycleRules != null) {
rules.addAll(
transform(
lifecycleRules,
new Function<LifecycleRule, Rule>() {
@Override
public Rule apply(LifecycleRule lifecycleRule) {
return lifecycleRule.toPb();
}
}));
}

if (!rules.isEmpty()) {
lifecycle.setRule(ImmutableList.copyOf(rules));
}
}

bucketPb.setLifecycle(lifecycle);
}

Expand Down
Expand Up @@ -799,6 +799,25 @@ public long getCurrentUploadOffset(String uploadId) {
}
}

@Override
public StorageObject queryCompletedResumableUpload(String uploadId, long totalBytes) {
try {
GenericUrl url = new GenericUrl(uploadId);
HttpRequest req = storage.getRequestFactory().buildPutRequest(url, new EmptyContent());
req.getHeaders().setContentRange(String.format("bytes */%s", totalBytes));
req.setParser(storage.getObjectParser());
HttpResponse response = req.execute();
// If the response is 200
if (response.getStatusCode() == 200) {
return response.parseAs(StorageObject.class);
} else {
throw buildStorageException(response.getStatusCode(), response.getStatusMessage());
}
} catch (IOException ex) {
throw translate(ex);
}
}

@Override
public StorageObject writeWithResponse(
String uploadId,
Expand Down Expand Up @@ -864,10 +883,7 @@ public StorageObject writeWithResponse(
if (exception != null) {
throw exception;
}
GoogleJsonError error = new GoogleJsonError();
error.setCode(code);
error.setMessage(message);
throw translate(error);
throw buildStorageException(code, message);
}
} catch (IOException ex) {
span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
Expand Down Expand Up @@ -914,10 +930,7 @@ public String open(StorageObject object, Map<Option, ?> options) {
setEncryptionHeaders(requestHeaders, "x-goog-encryption-", options);
HttpResponse response = httpRequest.execute();
if (response.getStatusCode() != 200) {
GoogleJsonError error = new GoogleJsonError();
error.setCode(response.getStatusCode());
error.setMessage(response.getStatusMessage());
throw translate(error);
throw buildStorageException(response.getStatusCode(), response.getStatusMessage());
}
return response.getHeaders().getLocation();
} catch (IOException ex) {
Expand Down Expand Up @@ -947,10 +960,7 @@ public String open(String signedURL) {
requestHeaders.set("x-goog-resumable", "start");
HttpResponse response = httpRequest.execute();
if (response.getStatusCode() != 201) {
GoogleJsonError error = new GoogleJsonError();
error.setCode(response.getStatusCode());
error.setMessage(response.getStatusMessage());
throw translate(error);
throw buildStorageException(response.getStatusCode(), response.getStatusMessage());
}
return response.getHeaders().getLocation();
} catch (IOException ex) {
Expand Down Expand Up @@ -1610,4 +1620,11 @@ public ServiceAccount getServiceAccount(String projectId) {
span.end(HttpStorageRpcSpans.END_SPAN_OPTIONS);
}
}

private static StorageException buildStorageException(int statusCode, String statusMessage) {
GoogleJsonError error = new GoogleJsonError();
error.setCode(statusCode);
error.setMessage(statusMessage);
return translate(error);
}
}
Expand Up @@ -337,6 +337,24 @@ void write(
*/
long getCurrentUploadOffset(String uploadId);

/**
* Attempts to retrieve the StorageObject from a completed resumable upload. When a resumable
* upload completes, the response will be the up-to-date StorageObject metadata. This up-to-date
* metadata can then be used to validate the total size of the object along with new generation
* and other information.
*
* <p>If for any reason, the response to the final PUT to a resumable upload is not received, this
* method can be used to query for the up-to-date StorageObject. If the upload is complete, this
* method can be used to access the StorageObject independently from any other liveness or
* conditional criteria requirements that are otherwise applicable when using {@link
* #get(StorageObject, Map)}.
*
* @param uploadId resumable upload ID URL
* @param totalBytes the total number of bytes that should have been written.
* @throws StorageException if the upload is incomplete or does not exist
*/
StorageObject queryCompletedResumableUpload(String uploadId, long totalBytes);

/**
* Writes the provided bytes to a storage object at the provided location. If {@code last=true}
* returns metadata of the updated object, otherwise returns null.
Expand Down
Expand Up @@ -144,6 +144,11 @@ public long getCurrentUploadOffset(String uploadId) {
throw new UnsupportedOperationException("Not implemented yet");
}

@Override
public StorageObject queryCompletedResumableUpload(String uploadId, long totalBytes) {
throw new UnsupportedOperationException("Not implemented yet");
}

@Override
public StorageObject writeWithResponse(
String uploadId,
Expand Down