Releases: bolshakov/stoplight
v5.0.3
v5.0.2
v5.0.1
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-adminOr if you're using Rails, just mount it in your routes:
Rails.application.routes.draw do
mount Stoplight::Admin => '/stoplights'
end
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
endYou 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]
endBetter 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 failureClock 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
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
- @olleolleolle made their first contribution in #218
- @ckornaros made their first contribution in #261
- @barthez made their first contribution in #265
Full Changelog: v4.1.0...v4.1.1
v3.0.2
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
What's Changed
- Expose CircuitBreaker#name and CircuitBreaker#state methods by @bolshakov in #198
- Bump version by @bolshakov in #199
Full Changelog: v4.0.0...v4.1.0
v4.0.0
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
endAnother 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
- Use actions/setup-ruby@v1 by ruby/setup-ruby@v1 by @bolshakov in #164
- Move data store specs into shared examples by @bolshakov in #166
- Update rubocop by @bolshakov in #167
- Use Redis in specs by @bolshakov in #173
- Add .idea to gitignore by @bolshakov in #175
- Shared specs for Stoplight::Light::Runnable by @bolshakov in #176
- Do not use the same error instance for tests by @bolshakov in #177
- Drop branded notifiers by @bolshakov in #174
- Test against different redis versions by @bolshakov in #179
- Implement window size by @bolshakov in #172
- feat | Light lock interface improvement by @Lokideos in #170
- Update gemfile lock by @bolshakov in #181
- Add stoplight-sentry by @bolshakov in #182
- Chore | Fix documentation for lockable methods by @Lokideos in #183
- Interface rework by @bolshakov in #180
- Update issue templates by @bolshakov in #189
- Create SECURITY.md by @bolshakov in #190
- Update rubocop.yml by @bolshakov in #191
- Update supported ruby version in ruby by @bolshakov in #193
- Update docs by @bolshakov in #194
- Specs for the circuit breaker module by @bolshakov in #195
Full Changelog: v3.0.1...v4.0.0
v3.0.1
What's Changed
#159 Fix race condition when sending notifications @Lokideos
#154 Drop HipChat support @BobbyMcWho @artygus
New Contributors
- @BobbyMcWho made their first contribution in #154
- @artygus made their first contribution in #157
- @Lokideos made their first contribution in #159
Full Changelog: v3.0.0...v3.0.1
v3.0.0
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