1 package com.guinetik.rr.http;
2
3 /**
4 * Exception thrown when a request is rejected due to an open circuit breaker.
5 *
6 * <p>This exception indicates that the downstream service is considered unhealthy and
7 * requests are being fast-failed to prevent cascading failures. It provides timing
8 * information to help callers decide when to retry.
9 *
10 * <h2>Handling Circuit Open</h2>
11 * <pre class="language-java"><code>
12 * try {
13 * User user = client.get("/users/1", User.class);
14 * } catch (CircuitBreakerOpenException e) {
15 * long waitTime = e.getEstimatedMillisUntilReset();
16 *
17 * if (waitTime > 0) {
18 * System.out.println("Service unavailable, retry in " + waitTime + "ms");
19 * // Schedule retry after waitTime
20 * } else {
21 * // Circuit should be half-open soon, retry immediately
22 * System.out.println("Circuit may reset soon, retrying...");
23 * }
24 * }
25 * </code></pre>
26 *
27 * <h2>Using with Fluent API</h2>
28 * <pre class="language-java"><code>
29 * Result<User, ApiError> result = client.fluent().get("/users/1", User.class);
30 *
31 * result.match(
32 * user -> System.out.println("Success"),
33 * error -> {
34 * if (error.isCircuitOpen()) {
35 * System.out.println("Circuit breaker is open");
36 * }
37 * }
38 * );
39 * </code></pre>
40 *
41 * @author guinetik <guinetik@gmail.com>
42 * @see CircuitBreakerClient
43 * @see RocketRestException
44 * @since 1.0.0
45 */
46 public class CircuitBreakerOpenException extends RocketRestException {
47
48 private static final long serialVersionUID = 1L;
49 private final long millisSinceLastFailure;
50 private final long resetTimeoutMs;
51
52 /**
53 * Creates a new CircuitBreakerOpenException with the specified message.
54 *
55 * @param message The error message
56 */
57 public CircuitBreakerOpenException(String message) {
58 this(message, 0, 0);
59 }
60
61 /**
62 * Creates a new CircuitBreakerOpenException with the specified message
63 * and timing information about when the circuit might reset.
64 *
65 * @param message The error message
66 * @param millisSinceLastFailure Milliseconds since the last failure
67 * @param resetTimeoutMs Milliseconds until the circuit will attempt to reset
68 */
69 public CircuitBreakerOpenException(String message, long millisSinceLastFailure, long resetTimeoutMs) {
70 super(message);
71 this.millisSinceLastFailure = millisSinceLastFailure;
72 this.resetTimeoutMs = resetTimeoutMs;
73 }
74
75 /**
76 * Creates a new CircuitBreakerOpenException with the specified message, cause,
77 * and timing information about when the circuit might reset.
78 *
79 * @param message The error message
80 * @param cause The cause of this exception
81 * @param millisSinceLastFailure Milliseconds since the last failure
82 * @param resetTimeoutMs Milliseconds until the circuit will attempt to reset
83 */
84 public CircuitBreakerOpenException(String message, Throwable cause,
85 long millisSinceLastFailure, long resetTimeoutMs) {
86 super(message, cause);
87 this.millisSinceLastFailure = millisSinceLastFailure;
88 this.resetTimeoutMs = resetTimeoutMs;
89 }
90
91 /**
92 * Gets the milliseconds since the last failure that caused the circuit to open.
93 *
94 * @return milliseconds since the last failure, or 0 if not available
95 */
96 public long getMillisSinceLastFailure() {
97 return millisSinceLastFailure;
98 }
99
100 /**
101 * Gets the configured timeout after which the circuit will try to reset.
102 *
103 * @return reset timeout in milliseconds, or 0 if not available
104 */
105 public long getResetTimeoutMs() {
106 return resetTimeoutMs;
107 }
108
109 /**
110 * Gets an estimated time in milliseconds until the circuit might reset.
111 * A negative value indicates the circuit should have already attempted to reset.
112 *
113 * @return estimated milliseconds until reset, or 0 if timing data is unavailable
114 */
115 public long getEstimatedMillisUntilReset() {
116 if (resetTimeoutMs > 0) {
117 return resetTimeoutMs - millisSinceLastFailure;
118 }
119 return 0;
120 }
121
122 /**
123 * Gets the underlying cause of the circuit opening.
124 * This may be null if the circuit was already open when the request was made.
125 *
126 * @return the cause exception that led to the circuit opening, or null if not available
127 */
128 @Override
129 public Throwable getCause() {
130 return super.getCause();
131 }
132 }