[CDAP-21257]Capturing Dataproc operations#16162
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces long-running operation (LRO) metric emission when Dataproc clusters transition to terminal states (such as RUNNING, FAILED, or NOT_EXISTS) from CREATING or DELETING. It adds a helper method to retrieve the latest operation by cluster and type, and includes corresponding unit tests. The review feedback highlights several issues: swapped arguments in a metric emission call, potential NullPointer or NoSuchElement exceptions when retrieving operations, manual gRPC status code mapping that can be simplified using standard utilities, the use of printStackTrace in production code, and leftover debug print statements in the test files.
1e9b980 to
8017a40
Compare
|
Please create a JIRA & attach to PR. |
|
Please add testing evidences & also change description. |
| this.profileName = profileName; | ||
| } | ||
|
|
||
| private final Map<String, Integer> metricCounts = new HashMap<>(); |
There was a problem hiding this comment.
Move variable declarations before function definitions.
Add more unit tests if possible |
| emitMetric(context, region, metricName, imageVersion, null); | ||
| } | ||
|
|
||
| public static void emitMetric(ProvisionerContext context, String region, |
There was a problem hiding this comment.
Why is this wrapper function with multiple arguments needed? It defeats the purpose of using Builder pattern for DataprocMetric.
There was a problem hiding this comment.
Removed the new created emitMetric and reusing the already available method.
| tags.put("imgVer", dataprocMetric.getImageVersion()); | ||
| } | ||
| if (!Strings.isNullOrEmpty(dataprocMetric.getOpType())) { | ||
| tags.put("op", dataprocMetric.getOpType()); |
There was a problem hiding this comment.
nit: operationType or opType or operation to be clear instead of 2 character label.
There was a problem hiding this comment.
In DataprocUtils.java, I mapped the tag key to "method" instead of "op" to keep it consistent.
| /** | ||
| * Get the latest operation by cluster name and operation type. | ||
| */ | ||
| public Optional<Operation> getLatestOperationByClusterAndType(String clusterName, String operationType) { |
There was a problem hiding this comment.
nit: Avoid redundancy in function naming. The argument names already specify cluster name and operation type.
| return Optional.of(operation); | ||
| } | ||
| } catch (ApiException e) { | ||
| LOG.warn("Failed to list operations with filter '{}': {}", filter, e.getMessage()); |
There was a problem hiding this comment.
Is it ok to ignore the exception and return Optional.empty() response?
How would caller know if the List Operation failed vs returned empty response?
There was a problem hiding this comment.
No, it is not okay to ignore the exception and return Optional.empty(). This is resolved by propagating the ApiException directly in the client method and handling it at the caller level inside the DataprocProvisioner class.
| String filter = String.format("clusterName=%s AND operationType=%s", clusterName, operationType); | ||
| try { | ||
| OperationsClient.ListOperationsPagedResponse response = | ||
| client.getOperationsClient().listOperations(resourceName, filter); |
There was a problem hiding this comment.
Should sort by option be used if the intent is to fetch the latest operation?
There was a problem hiding this comment.
No, we don't need to add that because the list operation by default returns the operation based on their creation id which means latest first.
| if (op.getDone()) { | ||
| if (op.hasError()) { | ||
| int rpcCode = op.getError().getCode(); | ||
| io.grpc.Status.Code grpcCode = io.grpc.Status.Code.INTERNAL; |
There was a problem hiding this comment.
Can be imported?
Similarly at other places.
| @Override | ||
| public void count(String metricName, int delta) {} | ||
| public void count(String metricName, int delta) { | ||
| metricCounts.put(metricName, metricCounts.getOrDefault(metricName, 0) + delta); |
There was a problem hiding this comment.
Just curious, why is this needed?
There was a problem hiding this comment.
This tracking is required so our new unit tests can capture and verify that the LRO transition metrics are being emitted correctly, the original mock context was returning empty method body that by default discard all the metrics.
| } | ||
| } | ||
|
|
||
| private boolean isTransitionToTerminal(ClusterStatus previous, ClusterStatus current) { |
There was a problem hiding this comment.
maybe rename it to isEndState ?
| } | ||
| } | ||
| } catch (Exception ex) { | ||
| LOG.warn("Failed to retrieve LRO operation metadata for metric emission: {}", ex.getMessage()); |
There was a problem hiding this comment.
why not print entire ex? Any particular reason why stack trace is omitted in this log?
There was a problem hiding this comment.
In the updated code, we are now correctly capturing the exception object.
| /** | ||
| * Get the latest operation by cluster name and operation type. | ||
| */ | ||
| public Optional<Operation> getLatestOperationByClusterAndType(String clusterName, String operationType) { |
There was a problem hiding this comment.
nit - getLatestOperation should be fine
There was a problem hiding this comment.
Noted. Corrected.
| return Optional.of(operation); | ||
| } | ||
| } catch (ApiException e) { | ||
| LOG.warn("Failed to list operations with filter '{}': {}", filter, e.getMessage()); |
There was a problem hiding this comment.
Any particular reason why we are neglecting stack trace?
There was a problem hiding this comment.
In the updated implementation, we no longer catch the exception inside DataprocClient.java. Instead, the client method now propagates the ApiException directly up to the caller, which is getLatestOperation in DataprocProvisioner, where are properly catching the stack trace.
| } | ||
|
|
||
| private boolean isTransitionToTerminal(ClusterStatus previous, ClusterStatus current) { | ||
| return (previous == ClusterStatus.CREATING || previous == ClusterStatus.DELETING) |
| if (opOpt.isPresent()) { | ||
| com.google.longrunning.Operation op = opOpt.get(); | ||
| if (op.getDone()) { | ||
| if (op.hasError()) { | ||
| int rpcCode = op.getError().getCode(); | ||
| io.grpc.Status.Code grpcCode = io.grpc.Status.Code.INTERNAL; | ||
| if (rpcCode >= 0 && rpcCode < io.grpc.Status.Code.values().length) { | ||
| grpcCode = io.grpc.Status.Code.values()[rpcCode]; | ||
| } | ||
| com.google.api.gax.rpc.StatusCode gaxStatusCode = | ||
| com.google.api.gax.grpc.GrpcStatusCode.of(grpcCode); | ||
| com.google.api.gax.rpc.ApiException apiException = | ||
| new com.google.api.gax.rpc.ApiException( | ||
| new Exception(op.getError().getMessage()), gaxStatusCode, false); | ||
| DataprocUtils.emitMetric(context, conf.getRegion(), metricName, | ||
| getImageVersion(context, conf), opType, new Exception(apiException)); | ||
| } else { | ||
| DataprocUtils.emitMetric(context, conf.getRegion(), getImageVersion(context, conf), | ||
| metricName, opType, null); | ||
| } | ||
| } |
There was a problem hiding this comment.
Please simplify code, use java best practices.
There is a deep nesting (try -> if -> if -> if -> if).
rpcCode, grpcCode -> var names are confusing.
| getImageVersion(context, conf), opType, new Exception(apiException)); | ||
| } else { | ||
| DataprocUtils.emitMetric(context, conf.getRegion(), getImageVersion(context, conf), | ||
| metricName, opType, null); |
There was a problem hiding this comment.
DataprocUtils.emitMetric(context, conf.getRegion(), metricName, getImageVersion(context, conf), opType, new Exception(apiException));
DataprocUtils.emitMetric(context, conf.getRegion(), getImageVersion(context, conf), metricName, opType, null);
the parameter ordering is correct?
There was a problem hiding this comment.
It was correct , we previously created a new emitMetric for case. In the updated code we are using the default emitMetric Method, so the ordering is adjusted accordingly.
e0dd0b5 to
faec3cf
Compare
| public Optional<Operation> getLatestOperation(String clusterName, String operationType) throws ApiException { | ||
| String projectId = conf.getProjectId(); | ||
| String region = conf.getRegion(); | ||
| String resourceName = String.format("projects/%s/regions/%s/operations", projectId, region); |
There was a problem hiding this comment.
nit - no need to initialize separate variables in line 830, 831 -> they are just used on line 832.
| client.getOperationsClient().listOperations(resourceName, filter); | ||
|
|
||
| Iterator<Operation> iterator = response.getPage().getValues().iterator(); | ||
| return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty(); |
There was a problem hiding this comment.
you can directly use :
return Optional.ofNullable(Iterables.getFirst(response.getPage().getValues(), null));
instead of defining variables and re-using in next line.
|
|
||
| @Nullable | ||
| private String getOpType(ClusterStatus status) { | ||
| if (status == ClusterStatus.CREATING) { |
There was a problem hiding this comment.
maybe use switch statement here?
| return null; | ||
| } | ||
|
|
||
| private Exception createApiException(Status error) { |
There was a problem hiding this comment.
can be simplified :
private ApiException createApiException(Status error) {
io.grpc.Status.Code grpcCode = io.grpc.Status.fromCodeValue(error.getCode()).getCode();
StatusCode gaxStatusCode = GrpcStatusCode.of(grpcCode);
return new ApiException(new Exception(error.getMessage()), gaxStatusCode, false);
}
it is fine to return ApiException (as per the method name)
| } | ||
|
|
||
| try { | ||
| Optional<Operation> opOpt = |
There was a problem hiding this comment.
can be renamed to a better name : you can use op as well?
opOpt looks more like opOptions
There was a problem hiding this comment.
renamed it as operation, not op as op name used in another variable.
| public Optional<Operation> getLatestOperation(String clusterName, String operationType) throws ApiException { | ||
| String projectId = conf.getProjectId(); | ||
| String region = conf.getRegion(); | ||
| String resourceName = String.format("projects/%s/regions/%s/operations", projectId, region); |
| private String profileName; | ||
| private ErrorCategory errorCategory; | ||
|
|
||
| private final Map<String, Integer> metricCounts = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Move above next to other private final variables.
|
|
||
| ClusterStatus currentStatus = provisioner.getClusterStatus(localContext, cdapClusterInput); | ||
| Assert.assertEquals(ClusterStatus.FAILED, currentStatus); | ||
|
|
There was a problem hiding this comment.
nit: remove extra new line
| String resourceName = String.format("projects/%s/regions/%s/operations", projectId, region); | ||
| String filter = String.format("clusterName=\"%s\" AND operationType=\"%s\"", clusterName, operationType); | ||
|
|
||
| OperationsClient.ListOperationsPagedResponse response = |
There was a problem hiding this comment.
Handle ApiException using DataprocUtils.handleApiException(...)
| } | ||
| } | ||
|
|
||
| private boolean isEndState(ClusterStatus previous, ClusterStatus current) { |
There was a problem hiding this comment.
nit: isTerminalState for consistency,
There was a problem hiding this comment.
Renamed it to hasReachedTerminalState().
| } | ||
|
|
||
| private boolean isEndState(ClusterStatus previous, ClusterStatus current) { | ||
| return TRANSITIONING_STATES.contains(previous) && TERMINAL_STATES.contains(current); |
There was a problem hiding this comment.
Does this method check if the state has now transitioned to terminal state? If so rename to hasReachedTerminalState() or something better. This function is not only checking for isEndState(), so naming is confusing.
There was a problem hiding this comment.
Renamed it to hasReachedTerminalState().
faec3cf to
381ab15
Compare
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import static io.cdap.cdap.runtime.spi.common.DataprocUtils.handleApiException; |
There was a problem hiding this comment.
No need to import this static function. It is used as multiple places in this file as DataprocUtils.handleApiException
There was a problem hiding this comment.
In general it is being done in the CDAP code base in the test classes or to import static constants / fields.
| } | ||
| } | ||
|
|
||
| private ApiException createApiException(Status error) { |
There was a problem hiding this comment.
Refactored it now.
| * @param previousStatus the status of the cluster before the transition | ||
| * @param clusterName the name of the cluster | ||
| */ | ||
| private void emitLroMetric(ProvisionerContext context, DataprocClient client, DataprocConf conf, |
There was a problem hiding this comment.
Please improve this function.
There was a problem hiding this comment.
Resolving the comment, based on offline discussion.
31797ca to
a93fff5
Compare
f31999d to
7793bf4
Compare
| } | ||
|
|
||
| DataprocUtils.emitMetric(context, builder.build()); | ||
| } catch (Exception ex) { |
| import java.util.function.Predicate; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import io.cdap.cdap.runtime.spi.provisioner.RetryableProvisionException; |
There was a problem hiding this comment.
This import is no longer needed removed.
| import org.mockito.Mock; | ||
| import org.mockito.Mockito; | ||
| import org.mockito.runners.MockitoJUnitRunner; | ||
| import org.powermock.api.mockito.PowerMockito; |
There was a problem hiding this comment.
Probably PowerMockito is not needed.
There was a problem hiding this comment.
Yes, it is not needed, removed.
7f6bcbb to
19f2895
Compare
| OperationsClient.ListOperationsPage page = Mockito.mock(OperationsClient.ListOperationsPage.class); | ||
| Mockito.when(listOperationsPagedResponse.getPage()).thenReturn(page); | ||
|
|
||
| Operation operation = Operation.newBuilder().setName("op-name").setDone(true).build(); |
There was a problem hiding this comment.
should we add a test case where multiple operations are returned?
There was a problem hiding this comment.
Multiple operations ? getLatestOperation just returns the latest operation, multiple operations are not returned.
There was a problem hiding this comment.
Done. Added the test case.
Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated Updated
19f2895 to
27af970
Compare
|



This pull request introduces background Long-Running Operation (LRO) tracking within the Dataproc Provisioner to record the final, asynchronous outcomes of cluster creations and deletions.
Code Changes
getLatestOperationto fetch background operation details from theregional projects.regions.operationsendpoint using cluster and task-type filters.ApiExceptionwrappers.Testing Evidence