Skip to content

Commit a62d44e

Browse files
authored
Add a reason field to QosException (#927)
Add a reason field to QosException
1 parent a1ca4c1 commit a62d44e

File tree

4 files changed

+228
-8
lines changed

4 files changed

+228
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type: improvement
2+
improvement:
3+
description: Add a reason field to QosException
4+
links:
5+
- https://github.com/palantir/conjure-java-runtime-api/pull/927

errors/src/main/java/com/palantir/conjure/java/api/errors/QosException.java

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,21 @@
3434
*/
3535
public abstract class QosException extends RuntimeException {
3636

37+
private final QosReason reason;
38+
3739
// Not meant for external subclassing.
38-
private QosException(String message) {
40+
private QosException(String message, QosReason reason) {
3941
super(message);
42+
this.reason = reason;
4043
}
4144

42-
private QosException(String message, Throwable cause) {
45+
private QosException(String message, Throwable cause, QosReason reason) {
4346
super(message, cause);
47+
this.reason = reason;
48+
}
49+
50+
public final QosReason getReason() {
51+
return reason;
4452
}
4553

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

72+
/**
73+
* Like {@link #throttle()}, but includes a reason.
74+
*/
75+
public static Throttle throttle(QosReason reason) {
76+
return new Throttle(Optional.empty(), reason);
77+
}
78+
6479
/**
6580
* Like {@link #throttle()}, but includes a cause.
6681
*/
6782
public static Throttle throttle(Throwable cause) {
6883
return new Throttle(Optional.empty(), cause);
6984
}
7085

86+
/**
87+
* Like {@link #throttle()}, but includes a reason, and a cause.
88+
*/
89+
public static Throttle throttle(QosReason reason, Throwable cause) {
90+
return new Throttle(Optional.empty(), cause, reason);
91+
}
92+
7193
/**
7294
* Like {@link #throttle()}, but additionally requests that the client wait for at least the given duration before
7395
* retrying the request.
@@ -76,13 +98,27 @@ public static Throttle throttle(Duration duration) {
7698
return new Throttle(Optional.of(duration));
7799
}
78100

101+
/**
102+
* Like {@link #throttle(Duration)}, but includes a reason.
103+
*/
104+
public static Throttle throttle(QosReason reason, Duration duration) {
105+
return new Throttle(Optional.of(duration), reason);
106+
}
107+
79108
/**
80109
* Like {@link #throttle(Duration)}, but includes a cause.
81110
*/
82111
public static Throttle throttle(Duration duration, Throwable cause) {
83112
return new Throttle(Optional.of(duration), cause);
84113
}
85114

115+
/**
116+
* Like {@link #throttle(Duration)}, but includes a reason, and a cause.
117+
*/
118+
public static Throttle throttle(QosReason reason, Duration duration, Throwable cause) {
119+
return new Throttle(Optional.of(duration), cause, reason);
120+
}
121+
86122
/**
87123
* Returns a {@link RetryOther} exception indicating that the calling client should retry against the given node of
88124
* this service.
@@ -91,13 +127,27 @@ public static RetryOther retryOther(URL redirectTo) {
91127
return new RetryOther(redirectTo);
92128
}
93129

130+
/**
131+
* Like {@link #retryOther(URL)}, but includes a reason.
132+
*/
133+
public static RetryOther retryOther(QosReason reason, URL redirectTo) {
134+
return new RetryOther(redirectTo, reason);
135+
}
136+
94137
/**
95138
* Like {@link #retryOther(URL)}, but includes a cause.
96139
*/
97140
public static RetryOther retryOther(URL redirectTo, Throwable cause) {
98141
return new RetryOther(redirectTo, cause);
99142
}
100143

144+
/**
145+
* Like {@link #retryOther(URL)}, but includes a reason, and a cause.
146+
*/
147+
public static RetryOther retryOther(QosReason reason, URL redirectTo, Throwable cause) {
148+
return new RetryOther(redirectTo, cause, reason);
149+
}
150+
101151
/**
102152
* An exception indicating that (this node of) this service is currently unavailable and the client may try again at
103153
* a later time, possibly against a different node of this service.
@@ -106,24 +156,53 @@ public static Unavailable unavailable() {
106156
return new Unavailable();
107157
}
108158

159+
/**
160+
* Like {@link #unavailable()}, but includes a reason.
161+
*/
162+
public static Unavailable unavailable(QosReason reason) {
163+
return new Unavailable(reason);
164+
}
165+
109166
/**
110167
* Like {@link #unavailable()}, but includes a cause.
111168
*/
112169
public static Unavailable unavailable(Throwable cause) {
113170
return new Unavailable(cause);
114171
}
115172

173+
/**
174+
* Like {@link #unavailable()}, but includes a reason, and a cause.
175+
*/
176+
public static Unavailable unavailable(QosReason reason, Throwable cause) {
177+
return new Unavailable(cause, reason);
178+
}
179+
116180
/** See {@link #throttle}. */
117181
public static final class Throttle extends QosException implements SafeLoggable {
182+
private static final QosReason DEFAULT_REASON = QosReason.of("qos-throttle");
183+
118184
private final Optional<Duration> retryAfter;
119185

120186
private Throttle(Optional<Duration> retryAfter) {
121-
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter);
187+
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, DEFAULT_REASON);
188+
this.retryAfter = retryAfter;
189+
}
190+
191+
private Throttle(Optional<Duration> retryAfter, QosReason reason) {
192+
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, reason);
122193
this.retryAfter = retryAfter;
123194
}
124195

125196
private Throttle(Optional<Duration> retryAfter, Throwable cause) {
126-
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, cause);
197+
super(
198+
"Suggesting request throttling with optional retryAfter duration: " + retryAfter,
199+
cause,
200+
DEFAULT_REASON);
201+
this.retryAfter = retryAfter;
202+
}
203+
204+
private Throttle(Optional<Duration> retryAfter, Throwable cause, QosReason reason) {
205+
super("Suggesting request throttling with optional retryAfter duration: " + retryAfter, cause, reason);
127206
this.retryAfter = retryAfter;
128207
}
129208

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

150229
/** See {@link #retryOther}. */
151230
public static final class RetryOther extends QosException implements SafeLoggable {
231+
private static final QosReason DEFAULT_REASON = QosReason.of("qos-retry-other");
232+
152233
private final URL redirectTo;
153234

154235
private RetryOther(URL redirectTo) {
155-
super("Suggesting request retry against: " + redirectTo.toString());
236+
super("Suggesting request retry against: " + redirectTo.toString(), DEFAULT_REASON);
237+
this.redirectTo = redirectTo;
238+
}
239+
240+
private RetryOther(URL redirectTo, QosReason reason) {
241+
super("Suggesting request retry against: " + redirectTo.toString(), reason);
156242
this.redirectTo = redirectTo;
157243
}
158244

159245
private RetryOther(URL redirectTo, Throwable cause) {
160-
super("Suggesting request retry against: " + redirectTo.toString(), cause);
246+
super("Suggesting request retry against: " + redirectTo.toString(), cause, DEFAULT_REASON);
247+
this.redirectTo = redirectTo;
248+
}
249+
250+
private RetryOther(URL redirectTo, Throwable cause, QosReason reason) {
251+
super("Suggesting request retry against: " + redirectTo.toString(), cause, reason);
161252
this.redirectTo = redirectTo;
162253
}
163254

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

185276
/** See {@link #unavailable}. */
186277
public static final class Unavailable extends QosException implements SafeLoggable {
278+
private static final QosReason DEFAULT_REASON = QosReason.of("qos-unavailable");
279+
187280
private static final String SERVER_UNAVAILABLE = "Server unavailable";
188281

189282
private Unavailable() {
190-
super(SERVER_UNAVAILABLE);
283+
super(SERVER_UNAVAILABLE, DEFAULT_REASON);
284+
}
285+
286+
private Unavailable(QosReason reason) {
287+
super(SERVER_UNAVAILABLE, reason);
191288
}
192289

193290
private Unavailable(Throwable cause) {
194-
super(SERVER_UNAVAILABLE, cause);
291+
super(SERVER_UNAVAILABLE, cause, DEFAULT_REASON);
292+
}
293+
294+
private Unavailable(Throwable cause, QosReason reason) {
295+
super(SERVER_UNAVAILABLE, cause, reason);
195296
}
196297

197298
@Override
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* (c) Copyright 2022 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.conjure.java.api.errors;
18+
19+
import com.google.errorprone.annotations.CompileTimeConstant;
20+
import com.palantir.logsafe.Preconditions;
21+
import com.palantir.logsafe.SafeArg;
22+
import java.util.Objects;
23+
import java.util.regex.Pattern;
24+
25+
/**
26+
* A class representing the reason why a {@link QosException} was created.
27+
*
28+
* Clients should create a relatively small number of static constant {@code Reason} objects, which are reused when
29+
* throwing QosExceptions. The string used to construct a {@code Reason} object should be able to be used as a metric
30+
* tag, for observability into {@link QosException} calls. As such, the string is constrained to have at most 50
31+
* lowercase alphanumeric characters, and hyphens (-).
32+
*/
33+
public final class QosReason {
34+
35+
@CompileTimeConstant
36+
private final String reason;
37+
38+
private static final Pattern PATTERN = Pattern.compile("^[a-z0-9\\-]{1,50}$");
39+
40+
private QosReason(@CompileTimeConstant String reason) {
41+
this.reason = reason;
42+
}
43+
44+
public static QosReason of(@CompileTimeConstant String reason) {
45+
Preconditions.checkArgument(
46+
PATTERN.matcher(reason).matches(),
47+
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
48+
+ "and hyphens (-).",
49+
SafeArg.of("reason", reason));
50+
return new QosReason(reason);
51+
}
52+
53+
@Override
54+
public String toString() {
55+
return reason;
56+
}
57+
58+
@Override
59+
public boolean equals(Object other) {
60+
if (other == this) {
61+
return true;
62+
} else if (!(other instanceof QosReason)) {
63+
return false;
64+
} else {
65+
QosReason otherReason = (QosReason) other;
66+
return Objects.equals(this.reason, otherReason.reason);
67+
}
68+
}
69+
70+
@Override
71+
public int hashCode() {
72+
return Objects.hashCode(this.reason);
73+
}
74+
}

errors/src/test/java/com/palantir/conjure/java/api/errors/QosExceptionTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package com.palantir.conjure.java.api.errors;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2021

22+
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
23+
import java.net.MalformedURLException;
2124
import java.net.URL;
2225
import org.junit.jupiter.api.Test;
2326

@@ -47,4 +50,41 @@ public Class visit(QosException.Unavailable exception) {
4750
.isEqualTo(QosException.RetryOther.class);
4851
assertThat(QosException.unavailable().accept(visitor)).isEqualTo(QosException.Unavailable.class);
4952
}
53+
54+
@Test
55+
public void testReason() {
56+
QosReason reason = QosReason.of("reason");
57+
assertThat(QosException.throttle(reason).getReason()).isEqualTo(reason);
58+
}
59+
60+
@Test
61+
public void testInvalidReason() {
62+
// Too long
63+
assertThatThrownBy(() -> QosReason.of("reason-reason-reason-reason-reason-reason-reason---"))
64+
.isInstanceOf(SafeIllegalArgumentException.class)
65+
.hasMessageContaining(
66+
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
67+
+ "and hyphens (-).");
68+
69+
// Unsupported characters
70+
assertThatThrownBy(() -> QosReason.of("reason?"))
71+
.isInstanceOf(SafeIllegalArgumentException.class)
72+
.hasMessageContaining(
73+
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, "
74+
+ "and hyphens (-).");
75+
76+
assertThatThrownBy(() -> QosReason.of("Reason"))
77+
.isInstanceOf(SafeIllegalArgumentException.class)
78+
.hasMessageContaining(
79+
"Reason must be at most 50 characters, and only contain lowercase letters, numbers, and"
80+
+ " hyphens (-).");
81+
}
82+
83+
@Test
84+
public void testDefaultReasons() throws MalformedURLException {
85+
assertThat(QosException.throttle().getReason().toString()).isEqualTo("qos-throttle");
86+
assertThat(QosException.retryOther(new URL("http://foo")).getReason().toString())
87+
.isEqualTo("qos-retry-other");
88+
assertThat(QosException.unavailable().getReason().toString()).isEqualTo("qos-unavailable");
89+
}
5090
}

0 commit comments

Comments
 (0)