Skip to content

A sync/async circuit breaker implementation in Python

License

Notifications You must be signed in to change notification settings

luizalabs/lasier

Repository files navigation

Lasier

A sync/async circuit breaker implementation

Build Status

According to Nygard on your masterpiece book Release It!:

[...] circuit breakers protect overeager gadget hounds from burning their houses down. The principle is the same: detect excess usage, fail first, and open the circuit. More abstractly, the circuit breaker exists to allow one subsystem (an electrical circuit) to fail (excessive current draw, possibly from a short circuit) without destroying the entire system (the house). Furthermore, once the danger has passed, the circuit breaker can be reset to restore full function to the system.

Requirements

  • Python >= 3.7

Instalation

Using pip:

pip install lasier

Usage

To use lasier circuit breaker you'll need a rule and a cache (the circuit state storage) instance

Rule

A Rule is the mechanism that define where circuit will open or close.

MaxFailuresRule

Rule to open circuit based on maximum number of failures

from lasier.circuit_breaker.rules import MaxFailuresRule

rule = MaxFailuresRule(
    max_failures=500,
    failure_cache_key='my_cb'
)
Arguments
Argument Definition
max_failures Maximum number of errors
failure_cache_key Cache key where the number of errors is incremented

PercentageFailuresRule

Rule to open circuit based on a percentage of failures

from lasier.circuit_breaker.rules import PercentageFailuresRule

rule = PercentageFailuresRule(
    max_failures_percentage=60,
    failure_cache_key='my_cb',
    min_accepted_requests=100,
    request_cache_key='my_cb_request'
)
Arguments
Argument Definition
max_failures_percentage Maximum percentage of errors
failure_cache_key Cache key where the number of errors is incremented
min_accepted_requests Minimum number of requests accepted to not open circuit breaker
request_cache_key Cache key where the number of requests is incremented

Circuit Breaker

You can use the Lasier circuit breaker with a context_manager f.ex:

from lasier.circuit_breaker.sync import CircuitBreaker

...

def some_protected_func():
    with CircuitBreaker(
        rule=rule,
        cache=cache,
        failure_exception=ValueError,
        catch_exceptions=(KeyError, TypeError)
    ):
        # some process

Or a decorator, f.ex:

from lasier.circuit_breaker.asyncio import circuit_breaker

...

@circuit_breaker(
    rule=rule,
    cache=cache,
    failure_exception=ValueError,
    catch_exceptions=(KeyError, TypeError)
)
async def some_protected_func():
    # some process

The sync and async implementations follow the same interface, so you only need to change the import path:

  • lasier.circuit_breaker.sync: for sync implementataion
  • lasier.circuit_breaker.asyncio: for async implementataion
Arguments
Argument Definition
rule Instance of class rule.
cache Instance of the circuit breaker state storage.
failure_exception Exception to be raised when it exceeds the maximum number of errors and when the circuit is open.
failure_timeout This value is set on first error. It is used to validate the number of errors by time. (seconds, default 60)
circuit_timeout Time that the circuit will be open. (seconds, default 60)
catch_exceptions List of exceptions catched to increase the number of errors.

WARNING: The args failure_timeout and circuit_timeout will be used on state storage commands so if you'll use libs that expects milliseconds instead of seconds on timeout arguments maybe you'll get yourself in trouble

Circuit state storage

Lasier works with a storage to register the current state of the circuit, number of failures, etc. That storage respects the follow interface:

from lasier.types import Timeout  # Timeout = Optional[Union[int, float]]


class Storage:

    def add(self, key: str, value: int, timeout: Timeout = None) -> None:
        pass

    def set(self, key: str, value: int, timeout: Timeout = None) -> None:
        pass

    def incr(self, key: str) -> int:
        pass

    def get(self, key: str) -> int:
        pass

    def expire(key: str, timeout: Timeout = None) -> None:
        pass

    def delete(self, key: str) -> None:
        pass

    def flushdb(self) -> None:
        pass

For async circuit breaker, lasier works with that same interface however with async syntax, f.ex: async def set(self, key=str, value=int, timeout=Optional[int])

So you can use any cache/storage that respects that interface.

Adapters

If you'll use Lasier with redis-py as cache, you can use lasier.adapters.caches.redis.RedisAdapter

from lasier.adapters.caches import RedisAdapter
from redis import Redis

cache = RedisAdapter(Redis(host='localhost', port=6479, db=0))

Implemented Adapters

Lib Adapter
redis-py lasier.adapters.caches.RedisAdapter
django-cache lasier.adapters.caches.DjangoAdapter
django-cache-async lasier.adapters.caches.DjangoAsyncAdapter
aiocache lasier.adapters.caches.AiocacheAdapter