Skip to content

[CDAP-21257]Capturing Dataproc operations#16162

Merged
123-komal merged 1 commit into
developfrom
dataproc-operations
Jul 9, 2026
Merged

[CDAP-21257]Capturing Dataproc operations#16162
123-komal merged 1 commit into
developfrom
dataproc-operations

Conversation

@123-komal

@123-komal 123-komal commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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

  • DataprocClient.java: Implemented getLatestOperation to fetch background operation details from the regional projects.regions.operations endpoint using cluster and task-type filters.
  • DataprocProvisioner.java: Added a low-complexity state observer to detect terminal transitions and emit the custom provisioner.operation.response.count metric. Error payloads are mapped cleanly to ApiException wrappers.
  • DataprocUtils.java: Refactored the metric tag registration to export dimensions under the standard "method" key.

Testing Evidence

  • DataprocClientTest.java: Verify successful operations queries and clean ApiException propagation.
  • DataprocProvisionerTest.java: Asserted that background operation metrics are successfully captured exactly once when the cluster transitions to terminal states (CREATING -> RUNNING and DELETING -> NOT_EXISTS).
  • MockProvisionerContext.java: Validated that metrics are recorded and aggregated accurately without race conditions in concurrent testing scenarios.

@123-komal 123-komal added the build Triggers github actions build label Jun 11, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@123-komal 123-komal force-pushed the dataproc-operations branch 3 times, most recently from 1e9b980 to 8017a40 Compare June 11, 2026 12:32
@123-komal 123-komal changed the title Collecting response code for Dataproc operations Capturing response code for Dataproc operations Jun 11, 2026
@sidhdirenge

Copy link
Copy Markdown
Contributor

Please create a JIRA & attach to PR.

@sidhdirenge

Copy link
Copy Markdown
Contributor

Please add testing evidences & also change description.

this.profileName = profileName;
}

private final Map<String, Integer> metricCounts = new HashMap<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move variable declarations before function definitions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@sidhdirenge

Copy link
Copy Markdown
Contributor

Quality Gate Failed Quality Gate failed

Failed conditions 53.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Add more unit tests if possible

emitMetric(context, region, metricName, imageVersion, null);
}

public static void emitMetric(ProvisionerContext context, String region,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this wrapper function with multiple arguments needed? It defeats the purpose of using Builder pattern for DataprocMetric.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: operationType or opType or operation to be clear instead of 2 character label.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Avoid redundancy in function naming. The argument names already specify cluster name and operation type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. Updated.

return Optional.of(operation);
}
} catch (ApiException e) {
LOG.warn("Failed to list operations with filter '{}': {}", filter, e.getMessage());

@vsethi09 vsethi09 Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should sort by option be used if the intent is to fetch the latest operation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

@vsethi09 vsethi09 Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be imported?

Similarly at other places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@Override
public void count(String metricName, int delta) {}
public void count(String metricName, int delta) {
metricCounts.put(metricName, metricCounts.getOrDefault(metricName, 0) + delta);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename it to isEndState ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}
} catch (Exception ex) {
LOG.warn("Failed to retrieve LRO operation metadata for metric emission: {}", ex.getMessage());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not print entire ex? Any particular reason why stack trace is omitted in this log?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - getLatestOperation should be fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. Corrected.

return Optional.of(operation);
}
} catch (ApiException e) {
LOG.warn("Failed to list operations with filter '{}': {}", filter, e.getMessage());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason why we are neglecting stack trace?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be simplified?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +518 to +538
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please simplify code, use java best practices.
There is a deep nesting (try -> if -> if -> if -> if).

rpcCode, grpcCode -> var names are confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplied.

getImageVersion(context, conf), opType, new Exception(apiException));
} else {
DataprocUtils.emitMetric(context, conf.getRegion(), getImageVersion(context, conf),
metricName, opType, null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@123-komal 123-komal force-pushed the dataproc-operations branch 3 times, most recently from e0dd0b5 to faec3cf Compare June 29, 2026 11:21
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - no need to initialize separate variables in line 830, 831 -> they are just used on line 832.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

client.getOperationsClient().listOperations(resourceName, filter);

Iterator<Operation> iterator = response.getPage().getValues().iterator();
return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can directly use :

return Optional.ofNullable(Iterables.getFirst(response.getPage().getValues(), null));

instead of defining variables and re-using in next line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


@Nullable
private String getOpType(ClusterStatus status) {
if (status == ClusterStatus.CREATING) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use switch statement here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return null;
}

private Exception createApiException(Status error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

try {
Optional<Operation> opOpt =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be renamed to a better name : you can use op as well?

opOpt looks more like opOptions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: operationName?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private String profileName;
private ErrorCategory errorCategory;

private final Map<String, Integer> metricCounts = new ConcurrentHashMap<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move above next to other private final variables.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


ClusterStatus currentStatus = provisioner.getClusterStatus(localContext, cdapClusterInput);
Assert.assertEquals(ClusterStatus.FAILED, currentStatus);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove extra new line

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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 =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle ApiException using DataprocUtils.handleApiException(...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. Done.

}
}

private boolean isEndState(ClusterStatus previous, ClusterStatus current) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: isTerminalState for consistency,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to hasReachedTerminalState().

}

private boolean isEndState(ClusterStatus previous, ClusterStatus current) {
return TRANSITIONING_STATES.contains(previous) && TERMINAL_STATES.contains(current);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to hasReachedTerminalState().

@123-komal 123-komal force-pushed the dataproc-operations branch from faec3cf to 381ab15 Compare June 29, 2026 16:48
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.cdap.cdap.runtime.spi.common.DataprocUtils.handleApiException;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to import this static function. It is used as multiple places in this file as DataprocUtils.handleApiException

@vsethi09 vsethi09 Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it is being done in the CDAP code base in the test classes or to import static constants / fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. Done.

}
}

private ApiException createApiException(Status error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please improve this function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving the comment, based on offline discussion.

@123-komal 123-komal force-pushed the dataproc-operations branch 3 times, most recently from 31797ca to a93fff5 Compare July 6, 2026 16:57
@123-komal 123-komal force-pushed the dataproc-operations branch 3 times, most recently from f31999d to 7793bf4 Compare July 8, 2026 07:11
@123-komal 123-komal changed the title Capturing response code for Dataproc operations Capturing Dataproc operations Jul 8, 2026
}

DataprocUtils.emitMetric(context, builder.build());
} catch (Exception ex) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch specific exceptions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

import java.util.function.Predicate;
import java.util.stream.Stream;

import io.cdap.cdap.runtime.spi.provisioner.RetryableProvisionException;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: import is misplaced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably PowerMockito is not needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is not needed, removed.

@123-komal 123-komal force-pushed the dataproc-operations branch 4 times, most recently from 7f6bcbb to 19f2895 Compare July 8, 2026 10:46
OperationsClient.ListOperationsPage page = Mockito.mock(OperationsClient.ListOperationsPage.class);
Mockito.when(listOperationsPagedResponse.getPage()).thenReturn(page);

Operation operation = Operation.newBuilder().setName("op-name").setDone(true).build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a test case where multiple operations are returned?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple operations ? getLatestOperation just returns the latest operation, multiple operations are not returned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added the test case.

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated

Updated
@123-komal 123-komal force-pushed the dataproc-operations branch from 19f2895 to 27af970 Compare July 8, 2026 15:02
@123-komal 123-komal changed the title Capturing Dataproc operations [CDAP-21257]Capturing Dataproc operations Jul 8, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

@123-komal 123-komal merged commit 905979b into develop Jul 9, 2026
19 checks passed
@123-komal 123-komal deleted the dataproc-operations branch July 9, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Triggers github actions build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants