Skip to content

Releases: bolshakov/stoplight

v5.0.3

26 Jun 15:01
v5.0.3
9217214

Choose a tag to compare

Bugfixes:

  • Resolve namespace conflict between sinatra and rake @Lokideos #369

Full Changelog: v5.0.2...v5.0.3

v5.0.2

22 Jun 08:44
v5.0.2
97cfeff

Choose a tag to compare

Bugfixes:

  • Adjust footer placement by @Lokideos #356
  • Do not raise an error during initialization if Redis is unavailable by @Lokideos #357
  • Stop sending reconfigure notifications on first configuration by @Lokideos #358

Full Changelog: v5.0.1...v5.0.2

v5.0.1

20 Jun 13:34
8c743cc

Choose a tag to compare

Stoplight 5.0 Release Notes

We're excited to share Stoplight 5.0 with you - this has been our biggest release yet! We've completely rewritten major
parts of the library to deliver better performance, cleaner APIs, and a much more pleasant developer experience. While
this does come with some breaking changes that will require migration work, we think you'll find the new features and
improvements well worth the effort.

What's New and Exciting

The headline feature is definitely our new simplified initialization syntax. Instead of chaining a bunch of method calls,
you can now configure everything upfront with keyword arguments:

light = Stoplight("Payment Service",
  threshold: 5,                           # 5 failures before turning red
  cool_off_time: 60,                      # Wait 60 seconds before attempting recovery  
  window_size: 300,                       # Only count failures in the last 5 minutes
  tracked_errors: [TimeoutError],         # Only count these errors
  skipped_errors: [ValidationError]       # Ignore these errors
)

Don't worry though - if you love the classic builder-style interface, it's sticking around. We have no plans to remove
it, so you can keep using the familiar pattern:

light = Stoplight.new("Payment Service")
    .with_threshold(5)
    .with_cool_off_time(60)
    .with_tracked_errors([TimeoutError])

Whichever style you prefer, it does not impart the Stoplight's execution overhead. However, if you prefer to instantiate
stoplight often, a new interface could offer up to 5x performance improvement over the classic one in some scenarios.

Admin UI Gets Some Love

The Stoplight Admin UI, which has been living as a separate gem for years, is now part of the main library. We've given
it a complete visual overhaul and fixed some long-standing issues. The biggest improvement is that it now actually
shows all your circuit breakers, including the green ones that were previously invisible. You'll also get much more
accurate real-time statistics thanks to our new data storage architecture.

Getting started with the Admin UI is super simple now. You can spin up a Docker container in seconds:

docker run --net=host -e REDIS_URL=redis://localhost:6378 bolshakov/stoplight-admin

Or if you're using Rails, just mount it in your routes:

Rails.application.routes.draw do
  mount Stoplight::Admin => '/stoplights'
end
Screenshot 2025-06-09 at 10 27 00

Error Handling Made Simple

The error handling configuration was always cumbersome in previous versions. The new approach is much more
straightforward. Instead of wrestling with complex error handler configurations, you can now just specify which errors
to track and which to ignore:

# Only track specific errors
api_gateway = Stoplight("API Gateway", tracked_errors: [TimeoutError, InternalServerError])

# Ignore certain errors completely  
action_light = Stoplight("Action", skipped_errors: [ValidationError, ActiveRecord::RecordNotFound])

This makes it much clearer what your circuit breaker is actually monitoring and responding to. By default all StandardErrors are tracked.

Redis Connection Pooling

For those of you running high-traffic applications, we've added proper Redis connection pooling support. This lets you
manage your Redis connections much more efficiently:

require "connection_pool"
pool = ConnectionPool.new(size: 5, timeout: 3) { Redis.new }
data_store = Stoplight::DataStore::Redis.new(pool)

Stoplight.configure do |config|
  config.data_store = data_store
end

You can still use a plain Redis client if connection pooling isn't needed for your use case.

Global Configuration

We've added a proper global configuration system that lets you set application-wide defaults. This is especially useful
if you have consistent patterns across all your circuit breakers:

Stoplight.configure do |config|
  config.threshold = 5
  config.cool_off_time = 30
  config.window_size = 60
  
  config.data_store = Stoplight::DataStore::Redis.new(redis)
  config.notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]
  
  config.tracked_errors = [StandardError]
  config.skipped_errors = [ActiveRecord::RecordNotFound]
end

Better Fallback Handling

Fallbacks work differently now - instead of configuring them on the light instance, you pass them directly to the
run method. This gives you much more flexibility since different operations can have completely different
fallback strategies:

light = Stoplight("Payment Gateway")

# Different fallbacks for different operations
light.run(-> { [] }) { get_invoices }      # Return empty array on failure
light.run(-> { 0 }) { get_credits }        # Return zero on failure

Clock Skew Detection

We've also added automatic clock skew detection for Redis deployments. Clock skew between your application servers and Redis can cause some confusing circuit breaker behavior - different instances might see different error counts or transition states at different times. Stoplight now automatically detects this issue and provides clear guidance on how to resolve it, with a warning when it occurs. The detection runs probabilistically (~1% of the time) to avoid any performance impact, and you can disable the warnings if needed.

Example warning:

Detected clock skew between Redis and the application server. Redis time: 1748277980, Application time: 1748274380. See https://github.com/bolshakov/stoplight/wiki/Clock-Skew-and-Stoplight-Reliability

Usage:

# Default behavior - warnings enabled
data_store = Stoplight::DataStore::Redis.new(redis)

# Disable warnings if needed (e.g., testing environments)
data_store = Stoplight::DataStore::Redis.new(redis, warn_on_clock_skew: false)

Under the Hood Improvements

We've completely rewritten the data storage layer using a metadata-driven approach. The old system was getting pretty
creaky, especially in distributed environments where you might have race conditions between different processes
updating circuit breaker state.

The new implementation uses Lua scripting for atomic operations, which eliminates the need for distributed locks while
actually improving performance. We're seeing fewer round trips to Redis and much better coordination when multiple
application instances are working with the same circuit breakers.

This foundation also sets us up for some exciting features we're planning for future releases, particularly around
more sophisticated error detection and recovery strategies.

Documentation and Developer Experience

We've completely rewritten our documentation to be more approachable for newcomers while still providing the depth
that advanced users need. The new README provides a visual walkthrough of concepts and includes numerous real-world examples.

We've also added a comprehensive BDD test suite that makes the library's behavior much clearer and gives us confidence when making changes. You can find an example of Cucumber report here

The Migration Story

Now for the part you're probably wondering about - yes, this release includes breaking changes that will require some
migration work. The global configuration API has changed significantly, some deprecated interfaces have been removed,
and the data storage format is incompatible with previous versions, which means existing circuit breaker states will be
lost during the upgrade.

We've put together [a comprehensive migration guide] that walks through each change step-by-step. While the migration
does require some effort, most of the changes are straightforward find-and-replace operations, and the new APIs are
much cleaner once you've made the switch.

The biggest impact is probably the Redis data incompatibility - when you upgrade, all your circuit breakers will reset
to green state. For most applications, this isn't a big deal since circuit breakers are designed to adapt quickly to
current conditions, but you'll want to plan your deployment accordingly.

What's Next

This release represents a major step forward for Stoplight and lays the groundwork for some exciting features we have
planned. The new data storage architecture opens up possibilities for much more sophisticated error detection strategies
and better observability features.

Full Changelog: v4.1.1...v5.0.1

v4.1.1

08 Feb 11:16
2a700f6

Choose a tag to compare

What's Changed

  • Add link for Honeybadger Notifiers gem by @ckornaros in #261
  • Ensure array is returned when cleaning failures in memory by @barthez in #265

New Contributors

Full Changelog: v4.1.0...v4.1.1

v3.0.2

28 Aug 12:27

Choose a tag to compare

What's Changed

  • Add human-friendly error message when Stoplight is used a wrong way by @bolshakov in #211

Full Changelog: v3.0.1...v3.0.2

v4.1.0

20 Aug 13:37
717936c

Choose a tag to compare

What's Changed

Full Changelog: v4.0.0...v4.1.0

v4.0.0

18 Aug 22:25
e705e4a

Choose a tag to compare

Stoplight 4.0 🎉

The next major release has been published, bringing a number of improvements and new features! However, it also introduces breaking changes. As a result, we removed certain notifiers. However, upgrading from Stoplight 3.x to Stoplight 4.0 need not be difficult. In fact, we have listed all the necessary steps in the UPGRADING.md documentation.

New Features

Stoplight() interface has changed

We aim to make Stoplight's configuration sharable across the code. The following change, enables Stoplight to run different code blocks with the same configuration:

light = Stoplight('http-api').with_cool_off_time(300)
light.run { call_this }
light.run { call_that }

The Stoplight() returns an immutable Stoplight::CircuitBreaker object that can be safely reconfigured. It makes it compatible with various Dependency Injections systems:

notifier = Stoplight::Sentry::Notifier.new(Sentry)
API_STOPLIGHT = Stoplight('http-api')
  .with_notifiers([notifier])
  .with_threshold(10)

class GetChangeOrdersApi
  def initialize(circuit_breaker: API_STOPLIGHT)
    @circuit_breaker = circuit_breaker.with_fallback { [] }
  end
  
  def call(order_id:)
    @circuit_breaker.run do 
      # calls HTTP API 
    end
  end
end

class GetOrderApi
  def initialize(circuit_breaker: API_STOPLIGHT)
    @circuit_breaker = circuit_breaker.with_fallback { NullOrder.new }
  end

  def call(order_id:)
    @circuit_breaker.run do
      # calls HTTP API 
    end
  end
end

Another benefit is that now you can easily inspect the status of the circuit breaker without passing an empty block:

light.color 

Introducing Sliding Window Error Counting

By default, every recorded failure contributes to reaching the threshold, regardless of when it occurs, causing the Stoplight to turn red. In this release, to provide more flexibility, we've introduced a sliding window configuration using the #with_window_size method (#172) that allows you to control how errors are counted within a specified time frame. Here's how it works:

Let's say you set the window size to 2 seconds:

window_size_in_seconds = 2

light = Stoplight('example-threshold')
  .with_window_size(window_size_in_seconds)
  .with_threshold(1)
# => #<Stoplight::CircuitBreaker:...>

light.run { 1 / 0 }#=> #<ZeroDivisionError: divided by 0>
sleep(3) 
light.run { 1 / 0 }

Without the window size configuration, the second light.run { 1 / 0 } call will result in a Stoplight::Error::RedLight exception being raised, as the Stoplight transitions to the red state after the first call. With a sliding window of 2 seconds, only the errors that occur within the latest 2 seconds are considered. The first error causes the Stoplight to turn red, but after 3 seconds (when the second error occurs), the window has shifted, and the Stoplight switches to green state causing the error to raise again. This provides a way to focus on the most recent errors.

It's worth noting that the default window size is set to infinity, meaning that all failures are counted regardless of timing.

Stoplight Gains an Interface for Locking Lights

In earlier versions, when you wanted to lock lights, you had to access Stoplight's internals. Stoplight 4.0 brings a new user-friendly interface to both lock and unlock lights:

light.lock('red')
light.unlock
light.lock('green')

What's Changed

Full Changelog: v3.0.1...v4.0.0

v3.0.1

03 Dec 15:15
888cd6e

Choose a tag to compare

What's Changed

#159 Fix race condition when sending notifications @Lokideos
#154 Drop HipChat support @BobbyMcWho @artygus

New Contributors

Full Changelog: v3.0.0...v3.0.1

v3.0.0

23 Feb 15:18

Choose a tag to compare

What's Changed

#146 Drop ruby 2.5.x support by @bolshakov
#146 Add ruby 3.0.x support by @bolshakov
#150 Fix deprecated uses of Redis#pipelined by @casperisfine

Full Changelog: v2.2.1...v3.0.0

v2.2.0

24 Oct 15:18

Choose a tag to compare

  • #118 Added Pagerduty notifier
  • #123 Added Rollbar notifier