Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 11 additions & 8 deletions lib/ldclient-rb/impl/file_data/reloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

require "ldclient-rb/impl/file_data/document"
require "ldclient-rb/impl/file_data/merge"
require "ldclient-rb/impl/util"

require "digest"

Expand Down Expand Up @@ -54,15 +53,18 @@ class Reloader
# automatically. If zero or negative, there is no automatic retry.
# @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
#
def initialize(paths:, logger:, apply:, on_error: nil,
duplicate_keys_handling: DuplicateKeysHandling::FAIL,
skip_missing_paths: false,
debounce_delay: DEFAULT_DEBOUNCE_DELAY,
retry_delay: DEFAULT_RETRY_DELAY,
skip_unchanged: false)
skip_unchanged: false,
log_prefix: "[LDClient]")
@paths = paths
@logger = logger
@log_prefix = log_prefix
@apply = apply
@on_error = on_error
@duplicate_keys_handling = duplicate_keys_handling
Expand Down Expand Up @@ -152,9 +154,9 @@ def stop
break if action == :stop

if action == :reload
@logger.info { "[LDClient] Reloading flag data after detecting a change" }
@logger.info { "#{@log_prefix} Reloading flag data after detecting a change" }
else
@logger.debug { "[LDClient] Retrying flag data load after earlier failure" }
@logger.debug { "#{@log_prefix} Retrying flag data load after earlier failure" }
end
ok = reload(retrying: action == :retry)
@mutex.synchronize do
Expand All @@ -164,7 +166,8 @@ def stop
end
end
rescue => e
Util.log_exception(@logger, "Unexpected error in file data reloader", e)
@logger.error { "#{@log_prefix} Unexpected error in file data reloader: #{e.inspect}" }
@logger.debug { "#{@log_prefix} Exception trace: #{e.backtrace}" }
end

#
Expand Down Expand Up @@ -226,7 +229,7 @@ def stop
content = FileData.read_file(path)
rescue ReadError => e
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" }
files << FileSummary.new(path, false, 0, 0)
next
end
Expand Down Expand Up @@ -295,12 +298,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 false
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)
false
end
Expand Down
151 changes: 151 additions & 0 deletions lib/ldclient-rb/impl/integrations/file_override_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# frozen_string_literal: true

require "ldclient-rb/impl/file_data"
require "ldclient-rb/interfaces/overrides"

module LaunchDarkly
module Impl
module Integrations
#
# The file-based override source. It reads flag and segment overrides from one or more local
# files and reloads them as the files change. See
# {LaunchDarkly::Integrations::FileData.override_source} for the public API and its options.
#
# Flag overrides are currently experimental and subject to change.
#
# @private
#
class FileOverrideSource
include LaunchDarkly::Interfaces::Overrides::OverrideSource

LOG_PREFIX = "[LDClient] FileOverrideSource:"

#
# @param paths [Array<String>] absolute file paths, in precedence order
# @param duplicate_keys_handling [Symbol] `:fail` or `:ignore`
# @param change_detection [Symbol] `:polling` or `:watching`
# @param poll_interval [Numeric] seconds between examinations in polling mode
# @param logger [Logger]
#
def initialize(paths:, duplicate_keys_handling:, change_detection:, poll_interval:, logger:)
@paths = paths
@duplicate_keys_handling = duplicate_keys_handling
@change_detection = change_detection
@poll_interval = poll_interval
@logger = logger
@lock = Mutex.new
@reloader = nil
@change_detector = nil
@stopped = false
end

# @return [Array<String>]
attr_reader :paths

# @return [Symbol]
attr_reader :duplicate_keys_handling

# @return [Symbol]
attr_reader :change_detection

# @return [Numeric]
attr_reader :poll_interval

#
# Performs the initial load synchronously, so overrides present in the files are in effect
# when this method returns, then starts change detection. A file that does not exist yet
# contributes no overrides. A file that cannot be read or parsed is not fatal: the client
# runs with the last good overrides, the failure is logged, and the retry plus the change
# signal recover once the file is readable.
#
# (see LaunchDarkly::Interfaces::Overrides::OverrideSource#start)
#
def start(sink)
@lock.synchronize do
return if @stopped

@reloader = FileData::Reloader.new(
paths: @paths,
logger: @logger,
log_prefix: LOG_PREFIX,
duplicate_keys_handling: @duplicate_keys_handling,
skip_missing_paths: true,
skip_unchanged: true,
apply: lambda do |merged|
sink.set_overrides(merged.flags.values, merged.segments.values)
log_overrides_in_effect(merged)
end
)
end

@reloader.reload_now

@lock.synchronize do
return if @stopped

trigger = @reloader.method(:trigger)
@change_detector =
if @change_detection == :watching
FileData::Watcher.new(@paths, trigger, @logger)
else
FileData::Poller.new(@paths, @poll_interval, trigger, @logger)
end
end
end

# (see LaunchDarkly::Interfaces::Overrides::OverrideSource#stop)
def stop
reloader = nil
change_detector = nil
@lock.synchronize do
@stopped = true
reloader = @reloader
change_detector = @change_detector
@reloader = nil
@change_detector = nil
end
change_detector&.stop
reloader&.stop
end

#
# Reports the overrides now in effect and the file each came from. The reloader applies a
# snapshot only when the content changed, so this logs each change once.
#
# @param merged [LaunchDarkly::Impl::FileData::MergeResult]
#
private def log_overrides_in_effect(merged)
details = merged.files.map do |file|
if !file.present
"#{file.path}: absent"
elsif file.flags.zero? && file.segments.zero?
"#{file.path}: no entries"
else
"#{file.path}: #{counts_text(file.flags, file.segments)}"
end
end.join("; ")

if merged.empty?
@logger.info { "#{LOG_PREFIX} Flag overrides: none in effect (#{details})" }
else
@logger.info do
"#{LOG_PREFIX} Flag overrides in effect: #{counts_text(merged.flags.length, merged.segments.length)} (#{details})"
end
end
end

# Formats flag and segment counts, for example "2 flags, 1 segment".
private def counts_text(flags, segments)
parts = []
parts << pluralize(flags, "flag") if flags > 0
parts << pluralize(segments, "segment") if segments > 0
parts.join(", ")
end

private def pluralize(count, noun)
count == 1 ? "1 #{noun}" : "#{count} #{noun}s"
end
end
end
end
end
Loading
Loading