Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contract-tests/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ gem "puma", "~> 8.0"
gem "rackup", "~> 2.2"
gem 'sinatra', '>= 4.1'

# The file-based override source uses the listen gem for its watching change detection mode.
gem 'listen', '~> 3.3'

gem 'rubocop', '~> 1.37', group: 'development'
gem 'rubocop-performance', '~> 1.15', group: 'development'

Expand Down
22 changes: 22 additions & 0 deletions contract-tests/client_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ def initialize(log, config)
opts[:payload_filter_key] = data_system_config[:payloadFilter]
end

# Flag overrides are an option of the FDv2 data system configuration.
data_system.overrides(build_override_source(config[:overrides])) if config[:overrides]

opts[:data_system_config] = data_system.build
elsif config[:overrides]
raise ArgumentError, "flag overrides require the data system to be configured"
elsif config[:streaming]
streaming = config[:streaming]
opts[:stream_uri] = streaming[:baseUri] unless streaming[:baseUri].nil?
Expand Down Expand Up @@ -381,6 +386,23 @@ def close
builder
end

#
# Builds the file-based override source from the `overrides` block of the contract test
# configuration.
#
# @param overrides_config [Hash] the overrides configuration
# @return [Object] a builder for LaunchDarkly::DataSystem::ConfigBuilder#overrides
#
private def build_override_source(overrides_config)
options = { paths: overrides_config[:filePaths] }
if overrides_config[:duplicateKeysHandling]
options[:duplicate_keys_handling] = overrides_config[:duplicateKeysHandling].to_sym
end
options[:change_detection] = overrides_config[:changeDetection].to_sym if overrides_config[:changeDetection]
set_optional_time_prop(overrides_config, :pollIntervalMs, options, :poll_interval)
LaunchDarkly::Integrations::FileData.override_source(options)
end

#
# Builds a persistent data store from the contract test configuration.
#
Expand Down
3 changes: 2 additions & 1 deletion contract-tests/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}

set :bind, '0.0.0.0'
set :port, 9000
set :port, (ENV['PORT'] || 9000).to_i
set :logging, false

clients = {}
Expand Down Expand Up @@ -60,6 +60,7 @@
'wrapper',
'service-endpoints',
'tls:verify-peer',
'flag-overrides',
],
}.to_json
end
Expand Down
8 changes: 4 additions & 4 deletions lib/ldclient-rb/data_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ module LaunchDarkly
#
# # Use the default strategy (recommended)
# config = LaunchDarkly::Config.new(
# data_system: LaunchDarkly::DataSystem.default
# data_system_config: LaunchDarkly::DataSystem.default.build
# )
#
# # Use streaming only
# config = LaunchDarkly::Config.new(
# data_system: LaunchDarkly::DataSystem.streaming
# data_system_config: LaunchDarkly::DataSystem.streaming.build
# )
#
# # Use polling only
# config = LaunchDarkly::Config.new(
# data_system: LaunchDarkly::DataSystem.polling
# data_system_config: LaunchDarkly::DataSystem.polling.build
# )
#
# == Custom Configurations
Expand All @@ -52,7 +52,7 @@ module LaunchDarkly
# .initializers([polling])
# .synchronizers([streaming, polling])
#
# config = LaunchDarkly::Config.new(data_system: data_system)
# config = LaunchDarkly::Config.new(data_system_config: data_system.build)
#
module DataSystem
#
Expand Down
40 changes: 24 additions & 16 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def record_eval_event(
debug_until = nil,
prereq_of = nil,
sampling_ratio = nil,
exclude_from_summaries = false
exclude_from_summaries = false,
override_affected = false
)
end

Expand Down Expand Up @@ -176,10 +177,11 @@ def record_eval_event(
debug_until = nil,
prereq_of = nil,
sampling_ratio = nil,
exclude_from_summaries = false
exclude_from_summaries = false,
override_affected = false
)
post_to_inbox(LaunchDarkly::Impl::EvalEvent.new(timestamp, context, key, version, variation, value, reason,
default, track_events, debug_until, prereq_of, sampling_ratio, exclude_from_summaries))
default, track_events, debug_until, prereq_of, sampling_ratio, exclude_from_summaries, override_affected))
end

def record_identify_event(context)
Expand Down Expand Up @@ -328,8 +330,10 @@ def dispatch_event(event, outbox)
will_add_full_event = false
debug_event = nil
if event.is_a?(LaunchDarkly::Impl::EvalEvent)
will_add_full_event = event.track_events
if should_debug_event(event)
# An override-affected evaluation appears only in the summary counters. It produces no
# individual feature event and no debug event, whatever the flag's configuration requests.
will_add_full_event = event.track_events && !event.override_affected
if !event.override_affected && should_debug_event(event)
debug_event = LaunchDarkly::Impl::DebugEvent.new(event)
end
else
Expand Down Expand Up @@ -630,18 +634,22 @@ def make_output_events(events, summary)
summary.counters.each do |flagKey, flagInfo|
counters = []
flagInfo.versions.each do |version, variations|
variations.each do |variation, counter|
c = {
value: counter.value,
count: counter.count,
}
c[:variation] = variation unless variation.nil?
if version.nil?
c[:unknown] = true
else
c[:version] = version
variations.each do |variation, counters_by_marker|
counters_by_marker.each do |override_affected, counter|
c = {
value: counter.value,
count: counter.count,
}
c[:variation] = variation unless variation.nil?
if version.nil?
c[:unknown] = true
else
c[:version] = version
end
# The marker is present only for override-affected counters, like the unknown marker.
c[:overrideAffected] = true if override_affected
counters.push(c)
end
counters.push(c)
end
end
flags[flagKey] = { default: flagInfo.default, counters: counters, contextKinds: flagInfo.context_kinds.to_a }
Expand Down
15 changes: 13 additions & 2 deletions lib/ldclient-rb/impl/event_summarizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module LaunchDarkly
module Impl
EventSummary = Struct.new(:start_date, :end_date, :counters)

# The `versions` member is a hash of hashes: flag version, then variation index, then the
# override-affected marker (a boolean), to an EventSummaryFlagVariationCounter. The marker
# takes part in the key, so override-affected and other evaluations of the same flag,
# variation, and version accumulate into separate counters.
EventSummaryFlagInfo = Struct.new(:default, :versions, :context_kinds)

EventSummaryFlagVariationCounter = Struct.new(:value, :count)
Expand Down Expand Up @@ -39,9 +43,16 @@ def summarize_event(event)

counters_for_flag.context_kinds.merge(event.context.kinds)

variation_counter = counters_for_flag_version[event.variation]
counters_for_variation = counters_for_flag_version[event.variation]
if counters_for_variation.nil?
counters_for_variation = Hash.new
counters_for_flag_version[event.variation] = counters_for_variation
end

override_affected = event.override_affected
variation_counter = counters_for_variation[override_affected]
if variation_counter.nil?
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
counters_for_variation[override_affected] = EventSummaryFlagVariationCounter.new(event.value, 1)
else
variation_counter.count = variation_counter.count + 1
end
Expand Down
12 changes: 11 additions & 1 deletion lib/ldclient-rb/impl/event_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def initialize(timestamp, context, sampling_ratio = nil, exclude_from_summaries

class EvalEvent < Event
def initialize(timestamp, context, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false)
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false,
override_affected = false)
super(timestamp, context, sampling_ratio, exclude_from_summaries)
@key = key
@version = version
Expand All @@ -38,6 +39,7 @@ def initialize(timestamp, context, key, version = nil, variation = nil, value =
@track_events = track_events if track_events
@debug_until = debug_until if debug_until
@prereq_of = prereq_of if prereq_of
@override_affected = true if override_affected
end

attr_reader :key
Expand All @@ -49,6 +51,14 @@ def initialize(timestamp, context, key, version = nil, variation = nil, value =
attr_reader :track_events
attr_reader :debug_until
attr_reader :prereq_of

# True if an override affected the evaluation, directly or through a prerequisite or segment.
# Such an evaluation appears only in the summary counters, under a counter that carries the
# override-affected marker. It produces no individual feature event and no debug event.
# @return [Boolean]
def override_affected
@override_affected ? true : false
end
end

class MigrationOpEvent < Event
Expand Down
21 changes: 12 additions & 9 deletions lib/ldclient-rb/impl/file_data/reloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require "ldclient-rb/impl/file_data/document"
require "ldclient-rb/impl/file_data/merge"
require "ldclient-rb/impl/file_data/poller"
require "ldclient-rb/impl/util"

require "digest"

Expand Down Expand Up @@ -98,6 +97,7 @@ class Reloader
# is parsed as it is.
# @param skip_unchanged [Boolean] if true, `apply` is not invoked when the files' raw
# contents are byte-identical to the last successfully applied contents.
# @param log_prefix [String] the prefix of every log line this reloader writes
# @param clock [#call, nil] returns the seconds of a monotonic clock, for the time a file has
# been held back; nil for the system's. Scheduling always uses the system's clock.
#
Expand All @@ -107,9 +107,11 @@ def initialize(paths:, logger:, apply:, on_error: nil,
debounce_delay: DEFAULT_DEBOUNCE_DELAY,
retry_delay: DEFAULT_RETRY_DELAY,
skip_unchanged: false,
log_prefix: "[LDClient]",
clock: nil)
@paths = paths
@logger = logger
@log_prefix = log_prefix
@apply = apply
@on_error = on_error
@duplicate_keys_handling = duplicate_keys_handling
Expand Down Expand Up @@ -222,7 +224,7 @@ def stop

# An unexpected error must not end the worker: no reload would happen after it.
begin
@logger.info { "[LDClient] Reloading flag data after detecting a change" } if action == :reload
@logger.info { "#{@log_prefix} Reloading flag data after detecting a change" } if action == :reload
retry_in = reload(retrying: action == :retry)
@mutex.synchronize do
# A pending retry is superseded by this reload. The reload either succeeded, or it
Expand All @@ -241,7 +243,8 @@ def stop
end

private def log_unexpected_error(error)
Util.log_exception(@logger, "Unexpected error in file data reloader", error)
@logger.error { "#{@log_prefix} Unexpected error in file data reloader: #{error.inspect}" }
@logger.debug { "#{@log_prefix} Exception trace: #{error.backtrace}" }
end

#
Expand Down Expand Up @@ -297,9 +300,9 @@ def stop

if retrying
if @read_again_pending
@logger.debug { "[LDClient] Reading flag data again after a file was found empty" }
@logger.debug { "#{@log_prefix} Reading flag data again after a file was found empty" }
else
@logger.debug { "[LDClient] Retrying flag data load after earlier failure" }
@logger.debug { "#{@log_prefix} Retrying flag data load after earlier failure" }
end
end
@read_again_pending = false
Expand Down Expand Up @@ -350,7 +353,7 @@ def stop
reached << path
end
if e.missing && @skip_missing_paths
@logger.debug { "[LDClient] File #{path} does not exist; it contributes no data" }
@logger.debug { "#{@log_prefix} File #{path} does not exist; it contributes no data" }
had_content[path] = false
append_to_digest(digest, path, nil)
files << FileSummary.new(path, false, 0, 0)
Expand Down Expand Up @@ -521,7 +524,7 @@ def stop
retry_at = nil
paths.each do |path|
@logger.debug do
"[LDClient] File #{path} is empty, possibly while it is being rewritten; reading it again before applying it"
"#{@log_prefix} File #{path} is empty, possibly while it is being rewritten; reading it again before applying it"
end
empty = @empty_files[path]
over = [empty.since + @retry_delay, empty.first_held + MAX_HOLD_RETRY_DELAYS * @retry_delay].min
Expand All @@ -540,12 +543,12 @@ def stop
# level and do not re-invoke on_error.
message = error.message
if message == @last_error_message
@logger.debug { "[LDClient] Unable to load flags: #{message}" }
@logger.debug { "#{@log_prefix} Unable to load flags: #{message}" }
return @retry_delay
end

@last_error_message = message
@logger.error { "[LDClient] Unable to load flags: #{message}" }
@logger.error { "#{@log_prefix} Unable to load flags: #{message}" }
@on_error&.call(error)
@retry_delay
end
Expand Down
Loading
Loading