Skip to content

Commit

Permalink
Add a reason field to QosException (#927)
Browse files Browse the repository at this point in the history
Add a reason field to QosException
  • Loading branch information
mpritham committed Dec 13, 2022
1 parent a1ca4c1 commit a62d44e
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 8 deletions.
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-927.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Add a reason field to QosException
links:
- https://github.com/palantir/conjure-java-runtime-api/pull/927
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,21 @@
*/
public abstract class QosException extends RuntimeException {

private final QosReason reason;

// Not meant for external subclassing.
private QosException(String message) {
private QosException(String message, QosReason reason) {
super(message);
this.reason = reason;
}

private QosException(String message, Throwable cause) {
private QosException(String message, Throwable cause, QosReason reason) {
super(message, cause);
this.reason = reason;
}

public final QosReason getReason() {
return reason;
}

public abstract <T> T accept(Visitor<T> visitor);
Expand All @@ -61,13 +69,27 @@ public static Throttle throttle() {
return new Throttle(Optional.empty());
}

/**
* Like {@link #throttle()}, but includes a reason.
*/
public static Throttle throttle(QosReason reason) {
return new Throttle(Optional.empty(), reason);
}

/**
* Like {@link #throttle()}, but includes a cause.
*/
public static Throttle throttle(Throwable cause) {
return new Throttle(Optional.empty(), cause);
}

/**
* Like {@link #throttle()}, but includes a reason, and a cause.
*/
public static Throttle throttle(QosReason reason, Throwable cause) {
return new Throttle(Optional.empty(), cause, reason);
}

/**
* Like {@link #throttle()}, but additionally requests that the client wait for at least the given duration before
* retrying the request.
Expand All @@ -76,13 +98,27 @@ public static Throttle throttle(Duration duration) {
return new Throttle(Optional.of(duration));
}

/**
* Like {@link #throttle(Duration)}, but includes a reason.
*/
public static Throttle throttle(QosReason reason, Duration duration) {
return new Throttle(Optional.of(duration), reason);
}

/**
* Like {@link #throttle(Duration)}, but includes a cause.
*/
public static Throttle throttle(Duration duration, Throwable cause) {
return new Throttle(Optional.of(duration), cause);
}

/**
* Like {@link #throttle(Duration)}, but includes a reason, and a cause.
*/
public static Throttle throttle(QosReason reason, Duration duration, Throwable cause) {
return new Throttle(Optional.of(duration), cause, reason);
}

/**
* Returns a {@link RetryOther} exception indicating that the calling client should retry against the given node of
* this service.
Expand All @@ -91,13 +127,27 @@ public static RetryOther retryOther(URL redirectTo) {
return new RetryOther(redirectTo);
}

/**
* Like {@link #retryOther(URL)}, but includes a reason.
*/
public static RetryOther retryOther(QosReason reason, URL redirectTo) {
return new RetryOther(redirectTo, reason);
}

/**
* Like {@link #retryOther(URL)}, but includes a cause.
*/
public static RetryOther retryOther(URL redirectTo, Throwable cause) {
return new RetryOther(redirectTo, cause);
}

/**
* Like {@link #retryOther(URL)}, but includes a reason, and a cause.
*/
public static RetryOther retryOther(QosReason reason, URL redirectTo, Throwable cause) {
return new RetryOther(redirectTo, cause, reason);
}

/**
* An exception indicating that (this node of) this service is currently unavailable and the client may try again at
* a later time, possibly against a different node of this service.
Expand All @@ -106,24 +156,53 @@ public static Unavailable unavailable() {
return new Unavailable();
}

/**
* Like {@link #unavailable()}, but includes a reason.
*/
public static Unavailable unavailable(QosReason reason) {
return new Unavailable(reason);
}

/**
* Like {@link #unavailable()}, but includes a cause.
*/
public static Unavailable unavailable(Throwable cause) {
return new Unavailable(cause);
}

/**
* Like {@link #unavailable()}, but includes a reason, and a cause.
*/
public static Unavailable unavailable(QosReason reason, Throwable cause) {
return new Unavailable(cause, reason);
}

/** See {@link #throttle}. */
public static final class Throttle extends QosException implements SafeLoggable {
private static final QosReason DEFAULT_REASON = QosReason.of("qos-throttle");

private final Optional<Duration> retryAfter;

private Throttle(Optional<Duration> retryAfter) {
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter);
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, DEFAULT_REASON);
this.retryAfter = retryAfter;
}

private Throttle(Optional<Duration> retryAfter, QosReason reason) {
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, reason);
this.retryAfter = retryAfter;
}

private Throttle(Optional<Duration> retryAfter, Throwable cause) {
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, cause);
super(
"Suggesting request throttling with optional retryAfter duration: " + retryAfter,
cause,
DEFAULT_REASON);
this.retryAfter = retryAfter;
}

private Throttle(Optional<Duration> retryAfter, Throwable cause, QosReason reason) {
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, cause, reason);
this.retryAfter = retryAfter;
}

Expand All @@ -149,15 +228,27 @@ public List<Arg<?>> getArgs() {

/** See {@link #retryOther}. */
public static final class RetryOther extends QosException implements SafeLoggable {
private static final QosReason DEFAULT_REASON = QosReason.of("qos-retry-other");

private final URL redirectTo;

private RetryOther(URL redirectTo) {
super("Suggesting request retry against: " + redirectTo.toString());
super("Suggesting request retry against: " + redirectTo.toString(), DEFAULT_REASON);
this.redirectTo = redirectTo;
}

private RetryOther(URL redirectTo, QosReason reason) {
super("Suggesting request retry against: " + redirectTo.toString(), reason);
this.redirectTo = redirectTo;
}

private RetryOther(URL redirectTo, Throwable cause) {
super("Suggesting request retry against: " + redirectTo.toString(), cause);
super("Suggesting request retry against: " + redirectTo.toString(), cause, DEFAULT_REASON);
this.redirectTo = redirectTo;
}

private RetryOther(URL redirectTo, Throwable cause, QosReason reason) {
super("Suggesting request retry against: " + redirectTo.toString(), cause, reason);
this.redirectTo = redirectTo;
}

Expand All @@ -184,14 +275,24 @@ public List<Arg<?>> getArgs() {

/** See {@link #unavailable}. */
public static final class Unavailable extends QosException implements SafeLoggable {
private static final QosReason DEFAULT_REASON = QosReason.of("qos-unavailable");

private static final String SERVER_UNAVAILABLE = "Server unavailable";

private Unavailable() {
super(SERVER_UNAVAILABLE);
super(SERVER_UNAVAILABLE, DEFAULT_REASON);
}

private Unavailable(QosReason reason) {
super(SERVER_UNAVAILABLE, reason);
}

private Unavailable(Throwable cause) {
super(SERVER_UNAVAILABLE, cause);
super(SERVER_UNAVAILABLE, cause, DEFAULT_REASON);
}

private Unavailable(Throwable cause, QosReason reason) {
super(SERVER_UNAVAILABLE, cause, reason);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* (c) Copyright 2022 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure.java.api.errors;

import com.google.errorprone.annotations.CompileTimeConstant;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import java.util.Objects;
import java.util.regex.Pattern;

/**
* A class representing the reason why a {@link QosException} was created.
*
* Clients should create a relatively small number of static constant {@code Reason} objects, which are reused when
* throwing QosExceptions. The string used to construct a {@code Reason} object should be able to be used as a metric
* tag, for observability into {@link QosException} calls. As such, the string is constrained to have at most 50
* lowercase alphanumeric characters, and hyphens (-).
*/
public final class QosReason {

@CompileTimeConstant
private final String reason;

private static final Pattern PATTERN = Pattern.compile("^[a-z0-9\\-]{1,50}$");

private QosReason(@CompileTimeConstant String reason) {
this.reason = reason;
}

public static QosReason of(@CompileTimeConstant String reason) {
Preconditions.checkArgument(
PATTERN.matcher(reason).matches(),
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
+ "and hyphens (-).",
SafeArg.of("reason", reason));
return new QosReason(reason);
}

@Override
public String toString() {
return reason;
}

@Override
public boolean equals(Object other) {
if (other == this) {
return true;
} else if (!(other instanceof QosReason)) {
return false;
} else {
QosReason otherReason = (QosReason) other;
return Objects.equals(this.reason, otherReason.reason);
}
}

@Override
public int hashCode() {
return Objects.hashCode(this.reason);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package com.palantir.conjure.java.api.errors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -47,4 +50,41 @@ public Class visit(QosException.Unavailable exception) {
.isEqualTo(QosException.RetryOther.class);
assertThat(QosException.unavailable().accept(visitor)).isEqualTo(QosException.Unavailable.class);
}

@Test
public void testReason() {
QosReason reason = QosReason.of("reason");
assertThat(QosException.throttle(reason).getReason()).isEqualTo(reason);
}

@Test
public void testInvalidReason() {
// Too long
assertThatThrownBy(() -> QosReason.of("reason-reason-reason-reason-reason-reason-reason---"))
.isInstanceOf(SafeIllegalArgumentException.class)
.hasMessageContaining(
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
+ "and hyphens (-).");

// Unsupported characters
assertThatThrownBy(() -> QosReason.of("reason?"))
.isInstanceOf(SafeIllegalArgumentException.class)
.hasMessageContaining(
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
+ "and hyphens (-).");

assertThatThrownBy(() -> QosReason.of("Reason"))
.isInstanceOf(SafeIllegalArgumentException.class)
.hasMessageContaining(
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, and"
+ " hyphens (-).");
}

@Test
public void testDefaultReasons() throws MalformedURLException {
assertThat(QosException.throttle().getReason().toString()).isEqualTo("qos-throttle");
assertThat(QosException.retryOther(new URL("http://foo")).getReason().toString())
.isEqualTo("qos-retry-other");
assertThat(QosException.unavailable().getReason().toString()).isEqualTo("qos-unavailable");
}
}

0 comments on commit a62d44e

Please sign in to comment.