All notable changes to this project, from version 1.0.0 onward, will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
Unreleased
2.0.0-rc.1 - 2026-06-18
First release candidate for 2.0. The 2.0 line modernizes the project and introduces breaking changes. See the migration guide for a step-by-step upgrade from 1.x.
Added
- Documentation overhaul: a set of guides (Getting Started, the module front door, circuit breakers, retries, rate limiting, error handling, telemetry), a cheatsheet, and a step-by-step migration guide, all published on HexDocs.
- Introspection for circuit breaker state (issue #5):
ExternalService.available?/1,ExternalService.blown?/1, andExternalService.all_available?/1, plusavailable?/0andblown?/0on modules usingExternalService.Gateway. :telemetryevents for guarded calls:[:external_service, :call, :start | :stop | :exception](a span around each call),[:external_service, :call, :retry],[:external_service, :circuit_breaker, :blown], and[:external_service, :rate_limit, :sleep]. See theExternalServicemodule docs for measurements and metadata.RetryOptions.max_attemptsto bound the total number of attempts (initial plus retries), complementing the existing time-based:expiry.RetryOptions.jitterto control random jitter on retry delays (truefor +/- 10%, or a float proportion such as0.25).- Declarative module front door:
use ExternalServicegenerates a small wrapper (call/1,2,call!/1,2, async/stream variants,available?/0,blown?/0,reset/0,child_spec/1,start_link/1) around a service configured with validated:circuit_breaker/:rate_limit/:retryoptions. - A service now remembers the default retry options given to
start/2; the two-argumentcall/2(andcall!/2,call_async/2) use that default. - Option validation via NimbleOptions for
start/2andRetryOptions, with the accepted options rendered into the docs. - Structured error types (built on Errata):
ExternalService.RetriesExhausted,ExternalService.CircuitBreakerOpen, andExternalService.ServiceNotStarted. Each is an exception struct carrying a:context(always including the:service), anhttp_status/1, and JSON encoding, so the same value can be returned fromcall/3or raised bycall!/3.
Changed (breaking)
Error representation overhauled.
call/3now returns structured error structs instead of nested tuples, andcall!/3raises the same structs:Before (1.x) After (2.0) {:error, {:retries_exhausted, reason}}{:error, %ExternalService.RetriesExhausted{context: %{service: name, reason: reason}}}{:error, {:fuse_blown, name}}{:error, %ExternalService.CircuitBreakerOpen{context: %{service: name}}}{:error, {:fuse_not_found, name}}{:error, %ExternalService.ServiceNotStarted{context: %{service: name}}}raise ExternalService.RetriesExhaustedErrorraise ExternalService.RetriesExhaustedraise ExternalService.FuseBlownErrorraise ExternalService.CircuitBreakerOpenraise ExternalService.FuseNotFoundErrorraise ExternalService.ServiceNotStartedResults returned directly by the wrapped function (including its own
{:error, reason}values) are unchanged. See the migration guide for the full mapping.Configuration and terminology overhauled to drop the leaked "fuse" wording:
start/2now takescircuit_breaker: [tolerate:, within:, reset:, fault_injection:]andrate_limit: [limit:, per:](and an optionalretry:) instead offuse_strategy: {:standard, max, window}/fuse_refresh:and therate_limit: {limit, window}tuple. Options are validated by NimbleOptions.- The
fuse_nameargument/type is nowservice. reset_fuse/1is nowreset/1.
Retry options reshaped (
ExternalService.RetryOptions):backoffis now:exponential/:linearwith separate:baseand:factor, instead of{:exponential, delay}/{:linear, delay, factor}.randomizeis nowjitter.rescue_onlyis nowretry_on, and defaults to[]— raised exceptions are no longer retried by default (issue #7). List exception modules in:retry_onto retry on them.:retry_onnow also governs the circuit breaker: an exception that is not retried no longer melts the breaker (it propagates untouched), so a raised exception counts as a circuit-breaker failure only when its type is in:retry_on. Explicit:retry/{:retry, reason}return values always melt the breaker.call/3andcall!/3now also accept a keyword list of retry options. A keyword list is treated as per-call overrides: it is merged onto the service's configured:retrydefaults (overriding only the keys it lists and inheriting the rest). A%RetryOptions{}struct still replaces the defaults entirely.
use ExternalService.Gatewayis deprecated in favor ofuse ExternalService. It still works (emitting a deprecation warning) and keeps theexternal_call/*andreset_fuse/0names as aliases, but uses the same new option shape asuse ExternalService— the oldfuse: [...]options are no longer supported.
Removed (breaking)
- The
ExternalService.RetriesExhaustedError,ExternalService.FuseBlownError, andExternalService.FuseNotFoundErrorexception modules, replaced by the structured error types above.
Fixed
ExternalService.Gatewaynow applies thefuse: [strategy:, refresh:]options it was configured with. Previously these keys did not match the:fuse_strategy/:fuse_refreshkeys thatExternalService.start/2reads, so every gateway silently ran on the default circuit-breaker configuration.- Added a regression test for the
:fault_injectionstrategy (issue #4); the:fuse_monitorcrash no longer reproduces on fuse 2.5. - Rate limiting now works for a service whose name is any term, not only an atom
or binary. The rate-limit bucket name is now derived with
inspect/1; previously it usedModule.concat/2, which raised for names such as tuples (circuit breaker and retries already accepted any term).
Changed
- Raise the minimum Elixir requirement to
~> 1.15. - Modernize the build: refreshed dependency versions, added
nimble_optionsandtelemetry, ExDoc/Dialyxir bumps, GitHub Actions CI (test matrix, quality, and Dialyzer jobs), and Hex package/docs metadata cleanup. - Store per-service state in
:persistent_terminstead of an unsupervisedAgent, removing a process that could crash and was never linked to a supervisor.ExternalService.stop/1now accepts any term as a fuse name (matchingstart/2), not only atoms, and is idempotent — it is safe to call on a service that was never started or has already been stopped.
1.1.4 - 2024-01-04
Fixed
- Replace use of deprecated
System.stacktrace/0with__STACKTRACE__/0(PR #17 from @iperks)
[1.1.3] - 2023-05-12
Changed
- Update to retry 0.18.0
- Update ex_rated to 2.1
1.1.2 - 2021-09-30
Changed
- Make sleep function configurable (PR #11 from @doorgan)
1.1.1 - 2021-09-17
Changed
- Update to fuse 2.5
- Update ex_rated to 2.0
1.1.0 - 2021-09-17
Added
Changed
- Allow any term as fuse name (PR #10 from @doorgan)
1.0.1 - 2020-06-08
Added
- Add ability to reset fuses
- Add documentation for initialization and configuration of gateway modules
1.0.0 - 2020-06-05
Added
- Add new ExternalService.Gateway module for module-based service gateways.
- Add this changelog...better late than never!