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

Add a reason field to QosException #927

Merged
merged 3 commits into from
Dec 13, 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-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");
}
}