Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RemoteException.getMessage includes SerializableError parameters #633

Merged
merged 2 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-633.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: RemoteException.getMessage (unsafe message) includes SerializableError parameters. RemoteException.getLogMessage (safe) is not impacted.
links:
- https://github.com/palantir/conjure-java-runtime-api/pull/633
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
public final class RemoteException extends RuntimeException implements SafeLoggable {
private static final long serialVersionUID = 1L;

private final String message;
private final String stableMessage;
private final SerializableError error;
private final int status;
private final List<Arg<?>> args;
// Lazily evaluated based on the stableMessage, errorInstanceId, and args.
@SuppressWarnings("MutableException")
private String unsafeMessage;

/** Returns the error thrown by a remote process which caused an RPC call to fail. */
public SerializableError getError() {
Expand All @@ -44,17 +46,40 @@ public int getStatus() {

public RemoteException(SerializableError error, int status) {
this.stableMessage = error.errorCode().equals(error.errorName())
? String.format("RemoteException: %s", error.errorCode())
: String.format("RemoteException: %s (%s)", error.errorCode(), error.errorName());
this.message = this.stableMessage + " with instance ID " + error.errorInstanceId();
? "RemoteException: " + error.errorCode()
: "RemoteException: " + error.errorCode() + " (" + error.errorName() + ")";
this.error = error;
this.status = status;
this.args = Collections.singletonList(SafeArg.of("errorInstanceId", error.errorInstanceId()));
}

@Override
public String getMessage() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also add the new safety annotations to this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn’t hurt, although we already consider throwable.getMessage unsafe across the board

return message;
// This field is not used in most environments so the cost of computation may be avoided.
String messageValue = unsafeMessage;
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious, why do we need the messageValue helper variable instead of doing something like:

if (unsafeMessage == null) {
    unsafeMessage = renderUnsafeMessage();
}
return unsafeMessage;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Optimization, not likely a measurable difference, but we’re reading the field value once instead of twice. I find it cleaner to assert on the same value that’s returned as well (avoids a class of race condition bugs)

if (messageValue == null) {
messageValue = renderUnsafeMessage();
unsafeMessage = messageValue;
}
return messageValue;
}

private String renderUnsafeMessage() {
StringBuilder builder = new StringBuilder()
.append(stableMessage)
.append(" with instance ID ")
.append(error.errorInstanceId());
if (!error.parameters().isEmpty()) {
builder.append(": {");
error.parameters()
.forEach((name, unsafeValue) ->
builder.append(name).append('=').append(unsafeValue).append(", "));
// remove the trailing space
builder.setLength(builder.length() - 1);

Choose a reason for hiding this comment

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

Joiner? Does that allocate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case it's unlikely that the last character resulted in a buffer expansion, so reducing the length by 1 is a non-volatile write to the StringBuilder.count integer.

Joiner would work nicely if this didn't involve relatively complex prefix parameters -- we'd end up creating a bunch of intermediate strings.

// replace the trailing comma with a close curly brace
builder.setCharAt(builder.length() - 1, '}');
}
return builder.toString();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,19 @@ public void testJavaSerialization() {
}

@Test
public void testSuperMessage() {
public void testUnsafeMessage_differentCodeAndName() {
SerializableError error = new SerializableError.Builder()
.errorCode("errorCode")
.errorName("errorName")
.errorInstanceId("errorId")
.build();
assertThat(new RemoteException(error, 500).getMessage())
.isEqualTo("RemoteException: errorCode (errorName) with instance ID errorId");
}

error = new SerializableError.Builder()
@Test
public void testUnsafeMessage_sameCodeAndName() {
SerializableError error = new SerializableError.Builder()
.errorCode("errorCode")
.errorName("errorCode")
.errorInstanceId("errorId")
Expand All @@ -65,6 +68,31 @@ public void testSuperMessage() {
.isEqualTo("RemoteException: errorCode with instance ID errorId");
}

@Test
public void testUnsafeMessage_oneParameter() {
SerializableError error = new SerializableError.Builder()
.errorCode("errorCode")
.errorName("errorName")
.errorInstanceId("errorId")
.putParameters("foo", "bar")
.build();
assertThat(new RemoteException(error, 500).getMessage())
.isEqualTo("RemoteException: errorCode (errorName) with instance ID errorId: {foo=bar}");
}

@Test
public void testUnsafeMessage_multipleParameters() {
SerializableError error = new SerializableError.Builder()
.errorCode("errorCode")
.errorName("errorName")
.errorInstanceId("errorId")
.putParameters("a", "b")
.putParameters("c", "d")
.build();
assertThat(new RemoteException(error, 500).getMessage())
.isEqualTo("RemoteException: errorCode (errorName) with instance ID errorId: {a=b, c=d}");
}

@Test
public void testLogMessageMessage() {
SerializableError error = new SerializableError.Builder()
Expand All @@ -77,7 +105,19 @@ public void testLogMessageMessage() {
}

@Test
public void testArgsIsEmpty() {
public void testLogMessageMessageDoesNotIncludeParameters() {
SerializableError error = new SerializableError.Builder()
.errorCode("errorCode")
.errorName("errorName")
.errorInstanceId("errorId")
.putParameters("param", "value")
.build();
RemoteException remoteException = new RemoteException(error, 500);
assertThat(remoteException.getLogMessage()).isEqualTo("RemoteException: errorCode (errorName)");
}

@Test
public void testArgsContainsOnlyErrorInstanceId() {
RemoteException remoteException = new RemoteException(
new SerializableError.Builder()
.errorCode("errorCode")
Expand Down