No description, website, or topics provided.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci
src/resilience4clj_timelimiter
test
.gitignore
LICENSE
README.md
deps.edn
pom.xml
tests.edn

README.md

Resilience4Clj Time Limiter

CircleCI Clojars License Status

Resilience4clj is a lightweight fault tolerance library set built on top of GitHub's Resilience4j and inspired by Netflix Hystrix. It was designed for Clojure and functional programming with composability in mind.

Read more about the motivation and details of Resilience4clj here.

Resilience4Clj Time Limiter lets you decorate a function call with a specified time limit. If the function does not return within the established time, an exception is thrown. This is particularly useful for external dependencies that may be too slow and you have time budgets for them.

Table of Contents

Getting Started

Add resilience4clj/resilience4clj-timelimiter as a dependency to your deps.edn file:

resilience4clj/resilience4clj-timelimiter {:mvn/version "0.1.0"}

If you are using lein instead, add it as a dependency to your project.clj file:

[resilience4clj/resilience4clj-timelimiter "0.1.0"]

Require the library:

(require '[resilience4clj-timelimiter.core :as tl])

Then create a time limiter calling the function create:

(def limiter (tl/create))

Now you can decorate any function you have with the limiter you just defined.

For the sake of this example, let's create a function that takes 250ms to return:

(defn slow-hello []
  (Thread/sleep 250)
  "Hello World!")

The function decorate will take the slow function just above and the time limiter you created and return a time limited function:

(def limited-hello (tl/decorate slow-hello limiter))

When you call limited-hello, it should eval to "Hello World!" after 250ms:

(limited-hello) ;; => "Hello World!" (after 250ms)

The default configuration of the limiter is to wait 1000ms (1s) that's why nothing really exceptional happened. If you were to redefined slow-hello to take 1500ms though:

(defn slow-hello []
  (Thread/sleep 1500)
  "Hello World!")

Then calling limited-hello (after also redefining it) would yield an exception:

(limited-hello) ;; => throws ExceptionInfo

Limiter Settings

When creating a time limiter, you can fine tune two of its settings:

  1. :timeout-duration - the timeout for this limiter in milliseconds (i.e. 300). Default is 1000.
  2. :cancel-running-future? - when true the call to the decorated function will be canceled in case of a timeout. If false, the call will go through even in case of a timeout. However, in that case, the caller will still get an exception thrown. Default is true.

Changing :cancel-running-future? to false is particularly useful if you have an external call with a side effect that might be relevant later (i.e. updating a cache with external data - in that case you might prefer to fail the first time around if your time budget expires but still get the cache eventually updated when the external system returns).

These two options can be sent to create as a map. In the following example, any function decorated with limiter will timeout in 300ms and calls will not be canceled if a timeout occurs.

(def limiter (tl/create {:timeout-duration 300
                         :cancel-running-future? false}))

Exception Handling

When a timeout occurs, an instance of ExceptionInfo will be thrown. Its payload can be extracted with ex-data and the key :resilience4clj.anomaly/category will be set with :resilience4clj.anomaly/execution-timeout.

(try
  (slow-hello)
  (catch Throwable e
    (if (= :resilience4clj.anomaly/execution-timeout
           (-> e ex-data :resilience4clj.anomaly/category))
      (println "Call timed out!!!"))))

Composing Further

Resilience4clj is composed of several modules that easily compose together. For instance, if you are also using the circuit breaker module and assuming your import and basic settings look like this:

(ns my-app
  (:require [resilience4clj-circuitbreaker.core :as cb]
            [resilience4clj-timelimiter.core :as tl]))

;; create time limiter with default settings
(def limiter (tl/create))

;; create circuit breaker with default settings
(def breaker (cb/create "HelloService"))

;; slow function you want to limit
(defn slow-hello []
  (Thread/sleep 1500)
  "Hello World!")

Then you can create a protected call that combines both the time limiter and the circuit breaker:

(def protected-hello (-> slow-hello
                         (tl/decorate limiter)
                         (cb/decorate breaker)))

The resulting function on protected-hello will trigger the breaker in case of a timeout now.

Bugs

If you find a bug, submit a Github issue.

Help

This project is looking for team members who can help this project succeed! If you are interested in becoming a team member please open an issue.

License

Copyright © 2019 Tiago Luchini

Distributed under the MIT License.