Warm-up

Build the three states

10 minintermediate210 yrs

One concept, guided. Near-impossible to fail.

What this teaches

  • A breaker stops calls rather than retrying them
  • Half-open is how it learns the dependency recovered
  • One trial call per cool-down, and a failed trial re-opens immediately
  • Injecting the clock makes every transition testable and exact

Starter

Starter.java
import java.util.*;
import java.util.function.*;

/**
 * Warm-up: a circuit breaker is a small state machine. Build it.
 *
 * Note the Clock. Time is injected rather than slept on, so every transition
 * is exact — and that is how you should test a breaker in real code too. A
 * test with Thread.sleep in it is slow and flaky for no benefit.
 */
public class Starter {

    static class Dependency {
        boolean down = true;
        int calls = 0;

        String fetch() {
            calls++;
            if (down) throw new IllegalStateException("connect timed out");
            return "ok";
        }
    }

    static class OpenCircuitException extends RuntimeException {
        OpenCircuitException() { super("circuit is open"); }
    }

    static class Clock {
        long now = 0;
        void advance(long millis) { now += millis; }
    }

    enum State { CLOSED, OPEN, HALF_OPEN }

    // TODO 1: write the breaker.
    //
    //     static class CircuitBreaker {
    //         CircuitBreaker(int threshold, long openMillis, Clock clock)
    //         State state()
    //         <T> T call(Supplier<T> action)
    //     }
    //
    // Rules, in the order they matter:
    //   - CLOSED: call through. Count failures. At the threshold, go OPEN and
    //     record when.
    //   - OPEN: throw OpenCircuitException WITHOUT calling the dependency.
    //   - After openMillis has passed, state() reports HALF_OPEN.
    //   - HALF_OPEN: let exactly one call through. Success closes the breaker;
    //     failure re-opens it and restarts the cool-down.

    public static void main(String[] args) {
        Dependency dependency = new Dependency();
        int failures = 0;
        for (int i = 0; i < 20; i++) {
            try { dependency.fetch(); } catch (RuntimeException e) { failures++; }
        }
        System.out.println("no breaker: " + dependency.calls + " calls, " + failures + " failures");

        // TODO 2: run the same twenty attempts through your breaker with a
        // threshold of 3. Predict how many reach the dependency first.

        // TODO 3: the rejected calls still failed. Write one sentence saying
        // what the breaker bought, in terms of threads rather than errors.

        // TODO 4: advance the clock past the cool-down and print state().
        // Then make one call while the dependency is still down, and print
        // the state again. Explain why a failed trial does not get a second
        // attempt.

        // TODO 5: set dependency.down = false, advance the clock again, and
        // watch it close. Note that nothing was configured and nobody was
        // paged.

        // TODO 6: what happens to your breaker if the dependency HANGS
        // instead of throwing — no exception, no return? Say what has to
        // exist for the breaker to work at all.
    }
}

Run it locally:

cd exercises/java/resilience/circuit-breaker/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Done when

  • Your breaker opens after the threshold and rejects without calling
  • It reaches half-open by advancing the clock, not by sleeping
  • A failed trial re-opens it; a successful one closes it
  • You can say what the rejected calls bought, given they still failed

← Back to What does a circuit breaker actually do?