Skip to content

Commit 11a75b5

Browse files
Add support for listStatus() and delete() operations. Add contract tests for getFileStatus(), delete() and mkdir()
Some of the contract tests are disabled and will be enabled in a subsequent change
1 parent eb70f03 commit 11a75b5

23 files changed

+1506
-95
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.fs.gs;
20+
21+
import com.google.api.client.googleapis.json.GoogleJsonError;
22+
import com.google.api.client.googleapis.json.GoogleJsonError.ErrorInfo;
23+
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
24+
import com.google.api.client.http.HttpResponseException;
25+
import com.google.api.client.http.HttpStatusCodes;
26+
import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableList;
27+
import org.apache.hadoop.thirdparty.com.google.common.collect.Iterables;
28+
import java.io.IOException;
29+
import java.util.List;
30+
import javax.annotation.Nullable;
31+
32+
/**
33+
* Translates exceptions from API calls into higher-level meaning, while allowing injectability for
34+
* testing how API errors are handled.
35+
*/
36+
class ApiErrorExtractor {
37+
38+
/** Singleton instance of the ApiErrorExtractor. */
39+
public static final ApiErrorExtractor INSTANCE = new ApiErrorExtractor();
40+
41+
public static final int STATUS_CODE_RANGE_NOT_SATISFIABLE = 416;
42+
43+
public static final String GLOBAL_DOMAIN = "global";
44+
public static final String USAGE_LIMITS_DOMAIN = "usageLimits";
45+
46+
public static final String RATE_LIMITED_REASON = "rateLimitExceeded";
47+
public static final String USER_RATE_LIMITED_REASON = "userRateLimitExceeded";
48+
49+
public static final String QUOTA_EXCEEDED_REASON = "quotaExceeded";
50+
51+
// These come with "The account for ... has been disabled" message.
52+
public static final String ACCOUNT_DISABLED_REASON = "accountDisabled";
53+
54+
// These come with "Project marked for deletion" message.
55+
public static final String ACCESS_NOT_CONFIGURED_REASON = "accessNotConfigured";
56+
57+
// These are 400 error codes with "resource 'xyz' is not ready" message.
58+
// These sometimes happens when create operation is still in-flight but resource
59+
// representation is already available via get call.
60+
// Only explanation I could find for this is described here:
61+
// java/com/google/cloud/cluster/data/cognac/cognac.proto
62+
// with an example "because resource is being created in reconciler."
63+
public static final String RESOURCE_NOT_READY_REASON = "resourceNotReady";
64+
65+
// HTTP 413 with message "Value for field 'foo' is too large".
66+
public static final String FIELD_SIZE_TOO_LARGE_REASON = "fieldSizeTooLarge";
67+
68+
// HTTP 400 message for 'USER_PROJECT_MISSING' error.
69+
public static final String USER_PROJECT_MISSING_MESSAGE =
70+
"Bucket is a requester pays bucket but no user project provided.";
71+
72+
// The debugInfo field present on Errors collection in GoogleJsonException
73+
// as an unknown key.
74+
private static final String DEBUG_INFO_FIELD = "debugInfo";
75+
76+
/**
77+
* Determines if the given exception indicates intermittent request failure or failure caused by
78+
* user error.
79+
*/
80+
public boolean requestFailure(IOException e) {
81+
HttpResponseException httpException = getHttpResponseException(e);
82+
return httpException != null
83+
&& (accessDenied(httpException)
84+
|| badRequest(httpException)
85+
|| internalServerError(httpException)
86+
|| rateLimited(httpException)
87+
|| IoExceptionHelper.isSocketError(httpException)
88+
|| unauthorized(httpException));
89+
}
90+
91+
/**
92+
* Determines if the given exception indicates 'access denied'. Recursively checks getCause() if
93+
* outer exception isn't an instance of the correct class.
94+
*
95+
* <p>Warning: this method only checks for access denied status code, however this may include
96+
* potentially recoverable reason codes such as rate limiting. For alternative, see {@link
97+
* #accessDeniedNonRecoverable(IOException)}.
98+
*/
99+
public boolean accessDenied(IOException e) {
100+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_FORBIDDEN);
101+
}
102+
103+
/** Determines if the given exception indicates bad request. */
104+
public boolean badRequest(IOException e) {
105+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
106+
}
107+
108+
/**
109+
* Determines if the given exception indicates the request was unauthenticated. This can be caused
110+
* by attaching invalid credentials to a request.
111+
*/
112+
public boolean unauthorized(IOException e) {
113+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
114+
}
115+
116+
/**
117+
* Determines if the exception is a non-recoverable access denied code (such as account closed or
118+
* marked for deletion).
119+
*/
120+
public boolean accessDeniedNonRecoverable(IOException e) {
121+
ErrorInfo errorInfo = getErrorInfo(e);
122+
String reason = errorInfo != null ? errorInfo.getReason() : null;
123+
return ACCOUNT_DISABLED_REASON.equals(reason) || ACCESS_NOT_CONFIGURED_REASON.equals(reason);
124+
}
125+
126+
/** Determines if the exception is a client error. */
127+
public boolean clientError(IOException e) {
128+
HttpResponseException httpException = getHttpResponseException(e);
129+
return httpException != null && getHttpStatusCode(httpException) / 100 == 4;
130+
}
131+
132+
/** Determines if the exception is an internal server error. */
133+
public boolean internalServerError(IOException e) {
134+
HttpResponseException httpException = getHttpResponseException(e);
135+
return httpException != null && getHttpStatusCode(httpException) / 100 == 5;
136+
}
137+
138+
/**
139+
* Determines if the given exception indicates 'item already exists'. Recursively checks
140+
* getCause() if outer exception isn't an instance of the correct class.
141+
*/
142+
public boolean itemAlreadyExists(IOException e) {
143+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_CONFLICT);
144+
}
145+
146+
/**
147+
* Determines if the given exception indicates 'item not found'. Recursively checks getCause() if
148+
* outer exception isn't an instance of the correct class.
149+
*/
150+
public boolean itemNotFound(IOException e) {
151+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_NOT_FOUND);
152+
}
153+
154+
/**
155+
* Determines if the given exception indicates 'field size too large'. Recursively checks
156+
* getCause() if outer exception isn't an instance of the correct class.
157+
*/
158+
public boolean fieldSizeTooLarge(IOException e) {
159+
ErrorInfo errorInfo = getErrorInfo(e);
160+
return errorInfo != null && FIELD_SIZE_TOO_LARGE_REASON.equals(errorInfo.getReason());
161+
}
162+
163+
/**
164+
* Determines if the given exception indicates 'resource not ready'. Recursively checks getCause()
165+
* if outer exception isn't an instance of the correct class.
166+
*/
167+
public boolean resourceNotReady(IOException e) {
168+
ErrorInfo errorInfo = getErrorInfo(e);
169+
return errorInfo != null && RESOURCE_NOT_READY_REASON.equals(errorInfo.getReason());
170+
}
171+
172+
/**
173+
* Determines if the given IOException indicates 'precondition not met' Recursively checks
174+
* getCause() if outer exception isn't an instance of the correct class.
175+
*/
176+
public boolean preconditionNotMet(IOException e) {
177+
return recursiveCheckForCode(e, HttpStatusCodes.STATUS_CODE_PRECONDITION_FAILED);
178+
}
179+
180+
/**
181+
* Determines if the given exception indicates 'range not satisfiable'. Recursively checks
182+
* getCause() if outer exception isn't an instance of the correct class.
183+
*/
184+
public boolean rangeNotSatisfiable(IOException e) {
185+
return recursiveCheckForCode(e, STATUS_CODE_RANGE_NOT_SATISFIABLE);
186+
}
187+
188+
/**
189+
* Determines if a given Throwable is caused by a rate limit being applied. Recursively checks
190+
* getCause() if outer exception isn't an instance of the correct class.
191+
*
192+
* @param e The Throwable to check.
193+
* @return True if the Throwable is a result of rate limiting being applied.
194+
*/
195+
public boolean rateLimited(IOException e) {
196+
ErrorInfo errorInfo = getErrorInfo(e);
197+
if (errorInfo != null) {
198+
String domain = errorInfo.getDomain();
199+
boolean isRateLimitedOrGlobalDomain =
200+
USAGE_LIMITS_DOMAIN.equals(domain) || GLOBAL_DOMAIN.equals(domain);
201+
String reason = errorInfo.getReason();
202+
boolean isRateLimitedReason =
203+
RATE_LIMITED_REASON.equals(reason) || USER_RATE_LIMITED_REASON.equals(reason);
204+
return isRateLimitedOrGlobalDomain && isRateLimitedReason;
205+
}
206+
return false;
207+
}
208+
209+
/**
210+
* Determines if a given Throwable is caused by Quota Exceeded. Recursively checks getCause() if
211+
* outer exception isn't an instance of the correct class.
212+
*/
213+
public boolean quotaExceeded(IOException e) {
214+
ErrorInfo errorInfo = getErrorInfo(e);
215+
return errorInfo != null && QUOTA_EXCEEDED_REASON.equals(errorInfo.getReason());
216+
}
217+
218+
/**
219+
* Determines if the given exception indicates that 'userProject' is missing in request.
220+
* Recursively checks getCause() if outer exception isn't an instance of the correct class.
221+
*/
222+
public boolean userProjectMissing(IOException e) {
223+
GoogleJsonError jsonError = getJsonError(e);
224+
return jsonError != null
225+
&& jsonError.getCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST
226+
&& USER_PROJECT_MISSING_MESSAGE.equals(jsonError.getMessage());
227+
}
228+
229+
/** Extracts the error message. */
230+
public String getErrorMessage(IOException e) {
231+
// Prefer to use message from GJRE.
232+
GoogleJsonError jsonError = getJsonError(e);
233+
return jsonError == null ? e.getMessage() : jsonError.getMessage();
234+
}
235+
236+
/**
237+
* Converts the exception to a user-presentable error message. Specifically, extracts message
238+
* field for HTTP 4xx codes, and creates a generic "Internal Server Error" for HTTP 5xx codes.
239+
*
240+
* @param e the exception
241+
* @param action the description of the action being performed at the time of error.
242+
* @see #toUserPresentableMessage(IOException, String)
243+
*/
244+
public IOException toUserPresentableException(IOException e, String action) throws IOException {
245+
throw new IOException(toUserPresentableMessage(e, action), e);
246+
}
247+
248+
/**
249+
* Converts the exception to a user-presentable error message. Specifically, extracts message
250+
* field for HTTP 4xx codes, and creates a generic "Internal Server Error" for HTTP 5xx codes.
251+
*/
252+
public String toUserPresentableMessage(IOException e, @Nullable String action) {
253+
String message = "Internal server error";
254+
if (clientError(e)) {
255+
message = getErrorMessage(e);
256+
}
257+
return action == null
258+
? message
259+
: String.format("Encountered an error while %s: %s", action, message);
260+
}
261+
262+
/** See {@link #toUserPresentableMessage(IOException, String)}. */
263+
public String toUserPresentableMessage(IOException e) {
264+
return toUserPresentableMessage(e, null);
265+
}
266+
267+
@Nullable
268+
public String getDebugInfo(IOException e) {
269+
ErrorInfo errorInfo = getErrorInfo(e);
270+
return errorInfo != null ? (String) errorInfo.getUnknownKeys().get(DEBUG_INFO_FIELD) : null;
271+
}
272+
273+
/**
274+
* Returns HTTP status code from the given exception.
275+
*
276+
* <p>Note: GoogleJsonResponseException.getStatusCode() method is marked final therefore it cannot
277+
* be mocked using Mockito. We use this helper so that we can override it in tests.
278+
*/
279+
protected int getHttpStatusCode(HttpResponseException e) {
280+
return e.getStatusCode();
281+
}
282+
283+
/**
284+
* Get the first ErrorInfo from an IOException if it is an instance of
285+
* GoogleJsonResponseException, otherwise return null.
286+
*/
287+
@Nullable
288+
protected ErrorInfo getErrorInfo(IOException e) {
289+
GoogleJsonError jsonError = getJsonError(e);
290+
List<ErrorInfo> errors = jsonError != null ? jsonError.getErrors() : ImmutableList.of();
291+
return errors != null ? Iterables.getFirst(errors, null) : null;
292+
}
293+
294+
/** If the exception is a GoogleJsonResponseException, get the error details, else return null. */
295+
@Nullable
296+
protected GoogleJsonError getJsonError(IOException e) {
297+
GoogleJsonResponseException jsonException = getJsonResponseException(e);
298+
return jsonException == null ? null : jsonException.getDetails();
299+
}
300+
301+
/** Recursively checks getCause() if outer exception isn't an instance of the correct class. */
302+
protected boolean recursiveCheckForCode(IOException e, int code) {
303+
HttpResponseException httpException = getHttpResponseException(e);
304+
return httpException != null && getHttpStatusCode(httpException) == code;
305+
}
306+
307+
@Nullable
308+
public static GoogleJsonResponseException getJsonResponseException(Throwable throwable) {
309+
Throwable cause = throwable;
310+
while (cause != null) {
311+
if (cause instanceof GoogleJsonResponseException) {
312+
return (GoogleJsonResponseException) cause;
313+
}
314+
cause = cause.getCause();
315+
}
316+
return null;
317+
}
318+
319+
@Nullable
320+
public static HttpResponseException getHttpResponseException(Throwable throwable) {
321+
Throwable cause = throwable;
322+
while (cause != null) {
323+
if (cause instanceof HttpResponseException) {
324+
return (HttpResponseException) cause;
325+
}
326+
cause = cause.getCause();
327+
}
328+
return null;
329+
}
330+
}

0 commit comments

Comments
 (0)