Skip to content

feat: lro compute poc - #14051

Draft
nnicolee wants to merge 2 commits into
feat/lro-generic-error-propagationfrom
feat/lro-compute-poc
Draft

feat: lro compute poc#14051
nnicolee wants to merge 2 commits into
feat/lro-generic-error-propagationfrom
feat/lro-compute-poc

Conversation

@nnicolee

Copy link
Copy Markdown
Contributor

Overview

This draft PR is a Proof of Concept (POC) implementing Option C (Interface-driven Delegation) for Compute-specific REST LRO error propagation.

It defines a generic error parsing interface in gax-httpjson and updates the code generator (gapic-generator-java) to generate and register a type-safe parser implementation in the Compute client stub settings. This avoids runtime reflection overhead while keeping the generator footprint clean.

Changes Included

1. GAX Core Changes (gax-httpjson)

  • HttpJsonLroErrorParser.java: A new interface defining methods for custom LRO error parsing (parse and parseErrorMessage).
  • ProtoOperationTransformers.java: Updated ResponseTransformer to accept a nullable HttpJsonLroErrorParser instance. If present, it delegates the unpacking of custom REST LRO error metadata (such as Compute's Errors list) into standard ErrorDetails.

2. Generator Changes (gapic-generator-java)

  • ComputeLroErrorParserClassComposer.java: A new class composer that writes out the concrete ComputeLroErrorParser class in Compute client stub packages.
  • Composer.java: Triggers the generation of ComputeLroErrorParser specifically for packages starting with com.google.cloud.compute.v1.
  • RetrySettingsComposer.java: Modifies response transformer generation to instantiate and pass the new ComputeLroErrorParser when compiling stub settings.
  • JavaWriterVisitor.java: Adds parenthesis wrapping for unary negation expressions (e.g. !(response instanceof Operation)) to prevent syntax compilation errors in generated output.

@nnicolee
nnicolee changed the base branch from main to feat/lro-generic-error-propagation August 12, 2026 05:16

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

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.

Code Review

This pull request introduces support for propagating custom Long-Running Operation (LRO) error details across both gRPC and HTTP/JSON transports, including the generation of a custom LRO error parser for the Google Cloud Compute service. Feedback on these changes suggests falling back to the snapshot's error details if the custom parser returns null to prevent losing transport-level errors. Additionally, the parenthesis-wrapping logic in JavaWriterVisitor should be expanded to include cast and arithmetic expressions to avoid operator precedence bugs, and the hardcoded package checks for the Compute service in the composer classes should be refactored to use a more decoupled, metadata-driven approach.

Comment on lines +70 to +78
if (errorParser != null && operationSnapshot.getResponse() != null) {
details = errorParser.parse(operationSnapshot.getResponse());
String parsedMsg = errorParser.parseErrorMessage(operationSnapshot.getResponse());
if (parsedMsg != null && !parsedMsg.isEmpty()) {
errorMessage = parsedMsg;
}
} else {
details = operationSnapshot.getErrorDetails();
}

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.

high

If errorParser is configured but returns null (for example, if the response is not of the expected type or the error is a transport-level error rather than an application-level LRO error), the current implementation will result in details being null. This causes the exception to lose any transport-level ErrorDetails that might have been populated in operationSnapshot.getErrorDetails().

To prevent losing error details on fallback/transport errors, we should fall back to operationSnapshot.getErrorDetails() if details is still null after attempting to use the custom parser.

Suggested change
if (errorParser != null && operationSnapshot.getResponse() != null) {
details = errorParser.parse(operationSnapshot.getResponse());
String parsedMsg = errorParser.parseErrorMessage(operationSnapshot.getResponse());
if (parsedMsg != null && !parsedMsg.isEmpty()) {
errorMessage = parsedMsg;
}
} else {
details = operationSnapshot.getErrorDetails();
}
if (errorParser != null && operationSnapshot.getResponse() != null) {
details = errorParser.parse(operationSnapshot.getResponse());
String parsedMsg = errorParser.parseErrorMessage(operationSnapshot.getResponse());
if (parsedMsg != null && !parsedMsg.isEmpty()) {
errorMessage = parsedMsg;
}
}
if (details == null) {
details = operationSnapshot.getErrorDetails();
}

Comment on lines +539 to +541
boolean needsParen = unaryOperationExpr.expr() instanceof InstanceofExpr
|| unaryOperationExpr.expr() instanceof RelationalOperationExpr
|| unaryOperationExpr.expr() instanceof LogicalOperationExpr;

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.

medium

The current logic for needsParen only covers InstanceofExpr, RelationalOperationExpr, and LogicalOperationExpr. However, unary operators (such as logical negation ! or unary minus -) have higher precedence than most other expressions in Java.

To prevent operator precedence bugs or compilation errors, needsParen should also include other expression types that have lower precedence than unary operators, such as CastExpr (e.g., !((Boolean) x) vs !(Boolean) x which is a syntax error) and ArithmeticOperationExpr (e.g., -(a + b) vs -a + b which changes semantics).

    boolean needsParen = unaryOperationExpr.expr() instanceof InstanceofExpr
        || unaryOperationExpr.expr() instanceof RelationalOperationExpr
        || unaryOperationExpr.expr() instanceof LogicalOperationExpr
        || unaryOperationExpr.expr() instanceof CastExpr
        || unaryOperationExpr.expr() instanceof ArithmeticOperationExpr;

Comment on lines +106 to +109
if (s.pakkage().startsWith("com.google.cloud.compute.v1")) {
clazzes.add(
ComputeLroErrorParserClassComposer.instance().generate(context, s));
}

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.

medium

Hardcoding the package prefix "com.google.cloud.compute.v1" directly in the core Composer class introduces tight coupling to a specific service (Compute) and violates the Open-Closed Principle.

If other services in the future require custom LRO error parsing, this class will have to be modified again. Consider making this behavior configurable or metadata-driven (e.g., via a flag in GapicContext or service configuration) rather than hardcoding package names.

Comment on lines +331 to +341
if (service.pakkage().startsWith("com.google.cloud.compute.v1") && operationResponseTransformer.reference().pakkage().equals("com.google.api.gax.httpjson")) {
createArgs.add(
NewObjectExpr.builder()
.setType(
TypeNode.withReference(
VaporReference.builder()
.setName("ComputeLroErrorParser")
.setPakkage(service.pakkage() + ".stub")
.build()))
.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.

medium

Similar to the change in Composer.java, hardcoding the package prefix "com.google.cloud.compute.v1" here introduces tight coupling to the Compute service.

Consider abstracting this check by introducing a property on the Service or GapicContext model (e.g., service.hasCustomLroErrorParser() or similar) to determine if a custom LRO error parser should be instantiated and passed to the response transformer.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

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

See analysis details on SonarQube Cloud

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

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

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant