Skip to content

Commit

Permalink
Merge pull request #495 from makicamel/send-eager-loading-data
Browse files Browse the repository at this point in the history
[Proposal] Introduce send_deferred_eager_loading_data configuration to reduce the load on Redis during deployment
  • Loading branch information
danmayer authored Jan 24, 2024
2 parents d0b77ab + 998b1bd commit 8eee4c0
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 3 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,25 @@ end

### Avoiding Cache Stampede

If you have many servers and they all hit Redis at the same time you can see spikes in your Redis CPU, and memory. This is due to a concept called [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede). It is better to spread out the reporting across your servers. A simple way to do this is to add a random wiggle on your background reporting. This configuration option allows a wiggle. The right amount of wiggle depends on the number of servers you have and how willing you are to have delays in your coverage reporting. I would recommend at least 1 second per server. Note, the default wiggle is set to 30 seconds.
If you have many servers and they all hit Redis at the same time you can see spikes in your Redis CPU, and memory. This is due to a concept called [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede).

It is better to spread out the reporting across your servers. A simple way to do this is to add a random wiggle on your background reporting. This configuration option allows a wiggle. The right amount of wiggle depends on the number of servers you have and how willing you are to have delays in your coverage reporting. I would recommend at least 1 second per server. Note, the default wiggle is set to 30 seconds.

Add a wiggle (in seconds) to the background thread to avoid all your servers reporting at the same time:

`config.reporting_wiggle = 30`

Another way to avoid cache stampede is to omit some reporting on starting servers. Coverband stores the results of eager_loading to Redis at server startup. The eager_loading results are the same for all servers, so there is no need to save all results. By configuring the eager_loading results of some servers to be stored in Redis, we can reduce the load on Redis during deployment.

```ruby
# To omit reporting on starting servers, need to defer saving eager_loading data
config.defer_eager_loading_data = true
# Store eager_loading data on 5% of servers
config.send_deferred_eager_loading_data = rand(100) < 5
# Store eager_loading data on servers with the environment variable
config.send_deferred_eager_loading_data = ENV.fetch('ENABLE_EAGER_LOADING_COVERAGE', false)
```

### Redis Hash Store

Coverband on very high volume sites with many server processes reporting can have a race condition which can cause hit counts to be inaccurate. To resolve the race condition and reduce Ruby memory overhead we have introduced a new Redis storage option. This moves the some of the work from the Ruby processes to Redis. It is worth noting because of this, it has larger demands on the Redis server. So adjust your Redis instance accordingly. To help reduce the extra redis load you can also change the background reporting frequency.
Expand Down
2 changes: 1 addition & 1 deletion lib/coverband/collectors/coverage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def report_coverage
else
if @deferred_eager_loading_data && Coverband.configuration.defer_eager_loading_data?
toggle_eager_loading do
@store.save_report(@deferred_eager_loading_data)
@store.save_report(@deferred_eager_loading_data) if Coverband.configuration.send_deferred_eager_loading_data?
@deferred_eager_loading_data = nil
end
end
Expand Down
8 changes: 7 additions & 1 deletion lib/coverband/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class Configuration
attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
:s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
:service_test_mode, :process_type, :track_views, :redis_url,
:background_reporting_sleep_seconds, :reporting_wiggle
:background_reporting_sleep_seconds, :reporting_wiggle,
:send_deferred_eager_loading_data

attr_reader :track_gems, :ignore, :use_oneshot_lines_coverage

Expand Down Expand Up @@ -67,6 +68,7 @@ def reset
@background_reporting_enabled = true
@background_reporting_sleep_seconds = nil
@defer_eager_loading_data = false
@send_deferred_eager_loading_data = true
@test_env = nil
@web_enable_clear = false
@track_views = true
Expand Down Expand Up @@ -287,6 +289,10 @@ def defer_eager_loading_data?
@defer_eager_loading_data
end

def send_deferred_eager_loading_data?
@send_deferred_eager_loading_data
end

def service_disabled_dev_test_env?
return false unless service?

Expand Down
52 changes: 52 additions & 0 deletions test/integration/full_stack_send_deferred_eager_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require File.expand_path("../test_helper", File.dirname(__FILE__))
require "rack"

class FullStackSendDeferredEagerTest < Minitest::Test
REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION
TEST_RACK_APP = "../fake_app/basic_rack.rb"

def setup
super
Coverband::Collectors::Coverage.instance.reset_instance
Coverband.configure do |config|
config.background_reporting_enabled = false
config.track_gems = true
config.defer_eager_loading_data = true
config.send_deferred_eager_loading_data = false
end
Coverband.start
Coverband::Collectors::Coverage.instance.eager_loading!
@rack_file = require_unique_file "fake_app/basic_rack.rb"
Coverband.report_coverage
Coverband::Collectors::Coverage.instance.runtime!
end

test "call app" do
# eager loaded class coverage starts empty
Coverband.eager_loading_coverage!
expected = {}
assert_equal expected, Coverband.configuration.store.coverage

Coverband::Collectors::Coverage.instance.runtime!
request = Rack::MockRequest.env_for("/anything.json")
middleware = Coverband::BackgroundMiddleware.new(fake_app_with_lines)
results = middleware.call(request)
assert_equal "Hello Rack!", results.last
Coverband.report_coverage
expected = [nil, nil, 0, nil, 0, 0, 1, nil, nil]
assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"]

# eager loaded class coverage is skipped at first normal coverage report
Coverband.eager_loading_coverage!
expected = {}
assert_equal expected, Coverband.configuration.store.coverage
end

private

def fake_app_with_lines
@fake_app_with_lines ||= ::HelloWorld.new
end
end

0 comments on commit 8eee4c0

Please sign in to comment.