Skip to content

Commit 73a0446

Browse files
committed
Error Management catch known errors [GCSBucketCreate]
1 parent ee6cb2e commit 73a0446

File tree

3 files changed

+141
-74
lines changed

3 files changed

+141
-74
lines changed

src/main/java/io/cdap/plugin/gcp/common/GCPErrorDetailsProvider.java

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,23 @@
1616

1717
package io.cdap.plugin.gcp.common;
1818

19-
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
2019
import com.google.api.client.http.HttpResponseException;
21-
import com.google.common.base.Strings;
2220
import com.google.common.base.Throwables;
2321
import io.cdap.cdap.api.exception.ErrorCategory;
2422
import io.cdap.cdap.api.exception.ErrorCategory.ErrorCategoryEnum;
25-
import io.cdap.cdap.api.exception.ErrorCodeType;
2623
import io.cdap.cdap.api.exception.ErrorType;
2724
import io.cdap.cdap.api.exception.ErrorUtils;
2825
import io.cdap.cdap.api.exception.ProgramFailureException;
2926
import io.cdap.cdap.etl.api.exception.ErrorContext;
3027
import io.cdap.cdap.etl.api.exception.ErrorDetailsProvider;
3128

32-
import java.io.IOException;
3329
import java.util.List;
3430

3531
/**
3632
* A custom ErrorDetailsProvider for GCP plugins.
3733
*/
3834
public class GCPErrorDetailsProvider implements ErrorDetailsProvider {
39-
private static final String ERROR_MESSAGE_FORMAT = "Error occurred in the phase: '%s'. %s: %s";
35+
static final String ERROR_MESSAGE_FORMAT = "Error occurred in the phase: '%s'. %s: %s";
4036

4137
/**
4238
* Get a ProgramFailureException with the given error
@@ -53,7 +49,8 @@ public ProgramFailureException getExceptionDetails(Exception e, ErrorContext err
5349
return null;
5450
}
5551
if (t instanceof HttpResponseException) {
56-
return getProgramFailureException((HttpResponseException) t, errorContext);
52+
return GCPErrorDetailsProviderUtil.getProgramFailureException((HttpResponseException) t,
53+
getExternalDocumentationLink(), errorContext);
5754
}
5855
if (t instanceof IllegalArgumentException) {
5956
return getProgramFailureException((IllegalArgumentException) t, errorContext);
@@ -65,58 +62,6 @@ public ProgramFailureException getExceptionDetails(Exception e, ErrorContext err
6562
return null;
6663
}
6764

68-
/**
69-
* Get a ProgramFailureException with the given error
70-
* information from {@link HttpResponseException}.
71-
*
72-
* @param e The HttpResponseException to get the error information from.
73-
* @return A ProgramFailureException with the given error information.
74-
*/
75-
private ProgramFailureException getProgramFailureException(HttpResponseException e,
76-
ErrorContext errorContext) {
77-
Integer statusCode = e.getStatusCode();
78-
ErrorUtils.ActionErrorPair pair = ErrorUtils.getActionErrorByStatusCode(statusCode);
79-
String errorReason = String.format("%s %s. %s", e.getStatusCode(), e.getStatusMessage(),
80-
pair.getCorrectiveAction());
81-
82-
String errorMessage = e.getMessage();
83-
String externalDocumentationLink = null;
84-
if (e instanceof GoogleJsonResponseException) {
85-
errorMessage = getErrorMessage((GoogleJsonResponseException) e);
86-
87-
externalDocumentationLink = getExternalDocumentationLink();
88-
if (!Strings.isNullOrEmpty(externalDocumentationLink)) {
89-
90-
if (!errorReason.endsWith(".")) {
91-
errorReason = errorReason + ".";
92-
}
93-
errorReason = String.format("%s For more details, see %s", errorReason,
94-
externalDocumentationLink);
95-
}
96-
}
97-
98-
return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategoryEnum.PLUGIN),
99-
errorReason, String.format(ERROR_MESSAGE_FORMAT, errorContext.getPhase(),
100-
e.getClass().getName(), errorMessage),
101-
pair.getErrorType(), true, ErrorCodeType.HTTP, statusCode.toString(),
102-
externalDocumentationLink, e);
103-
}
104-
105-
private String getErrorMessage(GoogleJsonResponseException exception) {
106-
if (!Strings.isNullOrEmpty(exception.getMessage())) {
107-
return exception.getMessage();
108-
}
109-
if (exception.getDetails() != null) {
110-
try {
111-
return exception.getDetails().toPrettyString();
112-
} catch (IOException e) {
113-
return exception.getDetails().toString();
114-
}
115-
}
116-
return exception.getMessage();
117-
}
118-
119-
12065
/**
12166
* Get a ProgramFailureException with the given error
12267
* information from {@link IllegalArgumentException}.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright © 2025 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* 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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.plugin.gcp.common;
18+
19+
20+
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
21+
import com.google.api.client.http.HttpResponseException;
22+
import com.google.common.base.Strings;
23+
import com.google.common.base.Throwables;
24+
import io.cdap.cdap.api.exception.ErrorCategory;
25+
import io.cdap.cdap.api.exception.ErrorCodeType;
26+
import io.cdap.cdap.api.exception.ErrorType;
27+
import io.cdap.cdap.api.exception.ErrorUtils;
28+
import io.cdap.cdap.api.exception.ProgramFailureException;
29+
import io.cdap.cdap.etl.api.exception.ErrorContext;
30+
import java.io.IOException;
31+
import java.util.List;
32+
import javax.annotation.Nullable;
33+
34+
/**
35+
* Common functions for GCP error details provider related functionalities.
36+
*/
37+
public final class GCPErrorDetailsProviderUtil {
38+
39+
/**
40+
* Get a ProgramFailureException with the given error
41+
* information from {@link HttpResponseException}.
42+
*
43+
* @param e The HttpResponseException to get the error information from.
44+
* @return A ProgramFailureException with the given error information.
45+
*/
46+
public static ProgramFailureException getProgramFailureException(HttpResponseException e, String externalDocUrl,
47+
@Nullable ErrorContext errorContext) {
48+
Integer statusCode = e.getStatusCode();
49+
ErrorUtils.ActionErrorPair pair = ErrorUtils.getActionErrorByStatusCode(statusCode);
50+
String errorReason = String.format("%s %s. %s", e.getStatusCode(), e.getStatusMessage(),
51+
pair.getCorrectiveAction());
52+
String errorMessage = e.getMessage();
53+
String externalDocumentationLink = null;
54+
if (e instanceof GoogleJsonResponseException) {
55+
errorMessage = getErrorMessage((GoogleJsonResponseException) e);
56+
externalDocumentationLink = externalDocUrl;
57+
if (!Strings.isNullOrEmpty(externalDocumentationLink)) {
58+
if (!errorReason.endsWith(".")) {
59+
errorReason = errorReason + ".";
60+
}
61+
errorReason = String.format("%s For more details, see %s", errorReason, externalDocumentationLink);
62+
}
63+
}
64+
return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason,
65+
errorContext != null ?
66+
String.format(GCPErrorDetailsProvider.ERROR_MESSAGE_FORMAT, errorContext.getPhase(), e.getClass().getName(),
67+
errorMessage) : String.format("%s: %s", e.getClass().getName(), errorMessage), pair.getErrorType(), true,
68+
ErrorCodeType.HTTP, statusCode.toString(), externalDocumentationLink, e);
69+
}
70+
71+
public static ProgramFailureException getHttpResponseExceptionDetailsFromChain(Exception e, String errorReason,
72+
String externalDocUrl) {
73+
return getHttpResponseExceptionDetailsFromChain(e, errorReason, ErrorType.USER, true, externalDocUrl);
74+
}
75+
76+
public static ProgramFailureException getHttpResponseExceptionDetailsFromChain(Exception e, String errorReason,
77+
ErrorType errorType,
78+
boolean dependency,
79+
String externalDocUrl) {
80+
List<Throwable> causalChain = Throwables.getCausalChain(e);
81+
for (Throwable t : causalChain) {
82+
if (t instanceof ProgramFailureException) {
83+
// Avoid double wrap
84+
return (ProgramFailureException) t;
85+
}
86+
if (t instanceof HttpResponseException) {
87+
return getProgramFailureException((HttpResponseException) t, externalDocUrl, null);
88+
}
89+
}
90+
// If no HttpResponseException found in the causal chain, return generic program failure exception
91+
return ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason,
92+
String.format("%s %s: %s", errorReason, e.getClass().getName(), e.getMessage()), errorType, dependency, e);
93+
}
94+
95+
private static String getErrorMessage(GoogleJsonResponseException exception) {
96+
if (!Strings.isNullOrEmpty(exception.getMessage())) {
97+
return exception.getMessage();
98+
}
99+
if (exception.getDetails() != null) {
100+
try {
101+
return exception.getDetails().toPrettyString();
102+
} catch (IOException e) {
103+
return exception.getDetails().toString();
104+
}
105+
}
106+
return exception.getMessage();
107+
}
108+
}

src/main/java/io/cdap/plugin/gcp/gcs/actions/GCSBucketCreate.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,27 @@
1616

1717
package io.cdap.plugin.gcp.gcs.actions;
1818

19-
import com.google.api.pathtemplate.ValidationException;
2019
import com.google.auth.Credentials;
21-
import com.google.auth.oauth2.ServiceAccountCredentials;
2220
import com.google.cloud.kms.v1.CryptoKeyName;
2321
import com.google.cloud.storage.Bucket;
2422
import com.google.cloud.storage.Storage;
2523
import com.google.cloud.storage.StorageException;
26-
import com.google.common.annotations.VisibleForTesting;
27-
import com.google.common.base.Strings;
2824
import io.cdap.cdap.api.annotation.Description;
2925
import io.cdap.cdap.api.annotation.Macro;
3026
import io.cdap.cdap.api.annotation.Name;
3127
import io.cdap.cdap.api.annotation.Plugin;
32-
import io.cdap.cdap.etl.api.Arguments;
28+
import io.cdap.cdap.api.exception.ErrorCategory;
29+
import io.cdap.cdap.api.exception.ErrorType;
30+
import io.cdap.cdap.api.exception.ErrorUtils;
3331
import io.cdap.cdap.etl.api.FailureCollector;
3432
import io.cdap.cdap.etl.api.PipelineConfigurer;
3533
import io.cdap.cdap.etl.api.action.Action;
3634
import io.cdap.cdap.etl.api.action.ActionContext;
3735
import io.cdap.plugin.gcp.common.CmekUtils;
3836
import io.cdap.plugin.gcp.common.GCPConfig;
37+
import io.cdap.plugin.gcp.common.GCPErrorDetailsProviderUtil;
3938
import io.cdap.plugin.gcp.common.GCPUtils;
4039
import io.cdap.plugin.gcp.gcs.GCSPath;
41-
import io.cdap.plugin.gcp.gcs.sink.GCSBatchSink;
4240
import org.apache.hadoop.conf.Configuration;
4341
import org.apache.hadoop.fs.FileSystem;
4442
import org.apache.hadoop.fs.Path;
@@ -86,9 +84,16 @@ public void run(ActionContext context) throws Exception {
8684
return;
8785
}
8886
String serviceAccount = config.getServiceAccount();
89-
Credentials credentials = serviceAccount == null ?
90-
null : GCPUtils.loadServiceAccountCredentials(serviceAccount, isServiceAccountFilePath);
91-
87+
Credentials credentials = null;
88+
try {
89+
credentials = serviceAccount == null ? null : GCPUtils.loadServiceAccountCredentials(serviceAccount,
90+
isServiceAccountFilePath);
91+
} catch (IOException e) {
92+
String errorReason = "Failed to load service account credentials.";
93+
collector.addFailure(String.format("%s %s: %s", errorReason, e.getClass().getName(), e.getMessage()), null)
94+
.withStacktrace(e.getStackTrace());
95+
collector.getOrThrowException();
96+
}
9297
Map<String, String> map = GCPUtils.generateGCSAuthProperties(serviceAccount, config.getServiceAccountType());
9398
map.forEach(configuration::set);
9499

@@ -127,17 +132,20 @@ public void run(ActionContext context) throws Exception {
127132
bucket = storage.get(gcsPath.getBucket());
128133
} catch (StorageException e) {
129134
// Add more descriptive error message
130-
throw new RuntimeException(
131-
String.format("Unable to access or create bucket %s. ", gcsPath.getBucket())
132-
+ "Ensure you entered the correct bucket path and have permissions for it.", e);
135+
String errorReason = String.format("Unable to access or create bucket %s. ", gcsPath.getBucket()) +
136+
"Ensure you entered the correct bucket path and have permissions for it.";
137+
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, errorReason,
138+
GCPUtils.GCS_SUPPORTED_DOC_URL);
133139
}
134140
if (bucket == null) {
135141
GCPUtils.createBucket(storage, gcsPath.getBucket(), config.location, cmekKeyName);
136142
undoBucket.add(bucketPath);
137143
} else if (gcsPath.equals(bucketPath) && config.failIfExists()) {
138144
// if the gcs path is just a bucket, and it exists, fail the pipeline
139145
rollback = true;
140-
throw new Exception(String.format("Path %s already exists", gcsPath));
146+
String errorReason = String.format("Path %s already exists", gcsPath);
147+
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
148+
errorReason, errorReason, ErrorType.USER, true, null);
141149
}
142150
}
143151

@@ -146,7 +154,9 @@ public void run(ActionContext context) throws Exception {
146154
fs = gcsPath.getFileSystem(configuration);
147155
} catch (IOException e) {
148156
rollback = true;
149-
throw new Exception("Unable to get GCS filesystem handler. " + e.getMessage(), e);
157+
String errorReason = "Unable to get GCS filesystem handler.";
158+
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, errorReason, ErrorType.UNKNOWN,
159+
true, GCPUtils.GCS_SUPPORTED_DOC_URL);
150160
}
151161
if (!fs.exists(gcsPath)) {
152162
try {
@@ -156,12 +166,16 @@ public void run(ActionContext context) throws Exception {
156166
} catch (IOException e) {
157167
LOG.warn(String.format("Failed to create path '%s'", gcsPath));
158168
rollback = true;
159-
throw e;
169+
String errorReason = String.format("Failed to create path %s.", gcsPath);
170+
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, errorReason,
171+
ErrorType.UNKNOWN, true, GCPUtils.GCS_SUPPORTED_DOC_URL);
160172
}
161173
} else {
162174
if (config.failIfExists()) {
163175
rollback = true;
164-
throw new Exception(String.format("Path %s already exists", gcsPath));
176+
String errorReason = String.format("Path %s already exists", gcsPath);
177+
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
178+
errorReason, errorReason, ErrorType.SYSTEM, true, null);
165179
}
166180
}
167181
}

0 commit comments

Comments
 (0)