import ballerina/http;
import ballerina/lang.runtime;

// The circuit breaker looks for errors across a rolling time window.
// After the circuit is broken, it does not send requests to
// the backend until the `resetTime`.
http:Client backendClientEP = check new ("http://localhost:8080", {
            // Configuration options that control the behavior of the circuit
            // breaker.
            circuitBreaker: {
                // Failure calculation window. This is how long the circuit
                // breaker keeps the statistics for the operations.
                rollingWindow: {

                    // Time period in seconds for which the failure
                    // threshold is calculated.
                    timeWindow: 10,

                    // The granularity (in seconds) at which the time
                    // window slides. The `RollingWindow` is divided into
                    // buckets and slides by these increments.
                    bucketSize: 2,

                    // Minimum number of requests in the `RollingWindow` that
                    // will trip the circuit.
                    requestVolumeThreshold: 0

                },
                // The threshold for request failures.
                // When this threshold exceeds, the circuit trips. This is the
                // ratio between failures and total requests. The ratio is
                // calculated using the requests received within the given
                // rolling window.
                failureThreshold: 0.2,

                // The time period (in seconds) to wait before attempting to
                // make another request to the upstream service.
                resetTime: 10,

                // HTTP response status codes that are considered as failures
                statusCodes: [400, 404, 500]

            },
            timeout: 2
        }
    );

// Create an HTTP service bound to the endpoint (circuitBreakerEP).
service /cb on new http:Listener(9090) {

    resource function get .(http:Request request)
            returns http:Response|http:InternalServerError {
        var backendResponse = backendClientEP->forward("/hello", request);
        // If the `backendResponse` is an `http:Response`, it is sent back to
        // the client. If `backendResponse` is an `http:ClientError`, an
        // internal server error is returned to the client.
        if (backendResponse is http:Response) {
            return backendResponse;
        } else {
            return {body:backendResponse.message()};
        }

    }
}

int counter = 1;

// This sample service is used to mock connection timeouts and service outages.
// This should run separately from the `circuitBreakerDemo` service.
service /hello on new http:Listener(8080) {

    resource function get .() returns string|http:InternalServerError {
        if (counter % 5 == 0) {
            counter += 1;
            // Delay the response by 5 seconds to
            // mimic the network level delays.
            runtime:sleep(5);

            return "Hello World!!!";
        } else if (counter % 5 == 3) {
            counter += 1;
            return {body:"Error occurred while processing the request."};
        } else {
            counter += 1;
            return "Hello World!!!";
        }
    }
}

Circuit Breaker

The Circuit Breaker is used to gracefully handle network related errors, which occur when using the HTTP Client.

For more information on the underlying module, see the HTTP module.

import ballerina/http;
import ballerina/lang.runtime;
http:Client backendClientEP = check new ("http://localhost:8080", {

The circuit breaker looks for errors across a rolling time window. After the circuit is broken, it does not send requests to the backend until the resetTime.

            circuitBreaker: {

Configuration options that control the behavior of the circuit breaker.

                rollingWindow: {

Failure calculation window. This is how long the circuit breaker keeps the statistics for the operations.

                    timeWindow: 10,

Time period in seconds for which the failure threshold is calculated.

                    bucketSize: 2,

The granularity (in seconds) at which the time window slides. The RollingWindow is divided into buckets and slides by these increments.

                    requestVolumeThreshold: 0

Minimum number of requests in the RollingWindow that will trip the circuit.

                },
                failureThreshold: 0.2,

The threshold for request failures. When this threshold exceeds, the circuit trips. This is the ratio between failures and total requests. The ratio is calculated using the requests received within the given rolling window.

                resetTime: 10,

The time period (in seconds) to wait before attempting to make another request to the upstream service.

                statusCodes: [400, 404, 500]

HTTP response status codes that are considered as failures

            },
            timeout: 2
        }
    );
service /cb on new http:Listener(9090) {

Create an HTTP service bound to the endpoint (circuitBreakerEP).

    resource function get .(http:Request request)
            returns http:Response|http:InternalServerError {
        var backendResponse = backendClientEP->forward("/hello", request);
        if (backendResponse is http:Response) {
            return backendResponse;
        } else {
            return {body:backendResponse.message()};
        }

If the backendResponse is an http:Response, it is sent back to the client. If backendResponse is an http:ClientError, an internal server error is returned to the client.

    }
}
int counter = 1;
service /hello on new http:Listener(8080) {

This sample service is used to mock connection timeouts and service outages. This should run separately from the circuitBreakerDemo service.

    resource function get .() returns string|http:InternalServerError {
        if (counter % 5 == 0) {
            counter += 1;
            runtime:sleep(5);

Delay the response by 5 seconds to mimic the network level delays.

            return "Hello World!!!";
        } else if (counter % 5 == 3) {
            counter += 1;
            return {body:"Error occurred while processing the request."};
        } else {
            counter += 1;
            return "Hello World!!!";
        }
    }
}
# To start the services, navigate to the directory that contains the
# `.bal` file and use the `bal run` command below.
bal run http_circuit_breaker.bal
[ballerina/http] started HTTP/WS listener 0.0.0.0:9090
[ballerina/http] started HTTP/WS listener 0.0.0.0:8080
time = 2021-01-21 19:20:04,028 level = INFO  module = ballerina/http message = "CircuitBreaker failure threshold exceeded. Circuit tripped from CLOSE to OPEN state."
time = 2021-01-21 19:20:14,306 level = INFO  module = ballerina/http message = "CircuitBreaker reset timeout reached. Circuit switched from OPEN to HALF_OPEN state."
time = 2021-01-21 19:20:17,810 level = INFO  module = ballerina/http message = "CircuitBreaker trial run  was successful. Circuit switched from HALF_OPEN to CLOSE state."
time = 2021-01-21 19:20:36,295 level = ERROR module = "" message = "Error sending response from mock service" error = "Connection between remote client and host is closed"
time = 2021-01-21 19:22:38,052 = INFO  module = ballerina/http message = "CircuitBreaker failure threshold exceeded. Circuit tripped from CLOSE to OPEN state."
time = 2021-01-21 19:23:23,628 level = INFO  module = ballerina/http message = "CircuitBreaker reset timeout reached. Circuit switched from OPEN to HALF_OPEN state."
time = 2021-01-21 19:23:57,633 level = INFO  module = ballerina/http message = "CircuitBreaker trial run  was successful. Circuit switched from HALF_OPEN to CLOSE state."
# The first two requests complete without any errors.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:07:58 +0530
< server: ballerina
< content-length: 14
<
* Connection #0 to host localhost left intact
Hello World!!!
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:08:00 +0530
< server: ballerina
< content-length: 14
<
* Connection #0 to host localhost left intact
Hello World!!!
# The third request responds with a `500 Internal Server Error` because the
# mock service sends a `500` http status code when responding to every third
# request.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:08:00 +0530
< server: ballerina
< content-length: 53
<
* Connection #0 to host localhost left intact
Internal error occurred while processing the request.
# Subsequent requests fail immediately since the reset timeout period has not
# elapsed.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< content-length: 99
< server: ballerina
< date: Mon, 21 Sep 2020 17:08:02 +0530
<
* Connection #0 to host localhost left intact
Upstream service unavailable. Requests to upstream service will be suspended for 8.340 seconds.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< content-length: 99
< server: ballerina
< date: Mon, 21 Sep 2020 17:08:04 +0530
<
* Connection #0 to host localhost left intact
Upstream service unavailable. Requests to upstream service will be suspended for 6.079 seconds.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< content-length: 99
< server: ballerina
< date: Mon, 21 Sep 2020 17:08:07 +0530
<
* Connection #0 to host localhost left intact
Upstream service unavailable. Requests to upstream service will be suspended for 3.475 seconds.
# The request sent immediately after the timeout period expires, is the trial
# request. It is sent to see if the backend service is back to normal.
# If this request is successful, the circuit is set to `close` and normal
# functionality resumes.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:08:12 +0530
< server: ballerina
< content-length: 14
<
* Connection #0 to host localhost left intact
Hello World!!!
# The fifth request times out because the mock service times out when
# responding to every fifth request.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< content-length: 57
< server: ballerina
< date: Mon, 21 Sep 2020 17:08:17 +0530
<
* Connection #0 to host localhost left intact
Idle timeout triggered before initiating inbound response
# Subsequent requests fail immediately since the reset timeout period has not
# elapsed.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain
< content-length: 99
< server: ballerina
< date: Mon, 21 Sep 2020 17:08:22 +0530
<
* Connection #0 to host localhost left intact
Upstream service unavailable. Requests to upstream service will be suspended for 4.917 seconds.
# The request sent immediately after the timeout period expires, is the trial
# request. It is sent to see if the backend service is back to normal.
# If this request is successful, the circuit is set to `close` and normal
# functionality resumes.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:08:32 +0530
< server: ballerina
< content-length: 14
<
* Connection #0 to host localhost left intact
Hello World!!!
# Since the immediate request after the timeout expired was successful, the
# requests sent after that complete normally.
curl -v http://localhost:9090/cb
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET /cb HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/plain
< date: Mon, 21 Sep 2020 17:08:35 +0530
< server: ballerina
< content-length: 14
<
* Connection #0 to host localhost left intact
Hello World!!!