Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/actions/check/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ runs:
test_service_port: 9000
enable_persistence_tests: true
token: ${{ inputs.token }}
version: v3.0.0-alpha.3
version: v3.0.0-alpha.4
22 changes: 22 additions & 0 deletions contract-tests/client_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'net/http'
require 'launchdarkly-server-sdk'
require './big_segment_store_fixture'
require './flag_change_listener'
require './hook'
require 'http'

Expand Down Expand Up @@ -117,6 +118,8 @@ def initialize(log, config)
config[:credential],
LaunchDarkly::Config.new(opts),
startWaitTimeMs / 1_000.0)

@listeners = ListenerRegistry.new(@client.flag_tracker)
end

def initialized?
Expand Down Expand Up @@ -225,7 +228,26 @@ def log
@log
end

def register_flag_change_listener(params)
@listeners.register_flag_change_listener(params[:listenerId], params[:callbackUri])
end

def register_flag_value_change_listener(params)
context = LaunchDarkly::LDContext.create(params[:context])
@listeners.register_flag_value_change_listener(
params[:listenerId],
params[:flagKey],
context,
params[:callbackUri]
)
end

def unregister_listener(params)
@listeners.unregister(params[:listenerId])
end

def close
@listeners.close_all
@client.close
@log.info("Test ended")
end
Expand Down
127 changes: 127 additions & 0 deletions contract-tests/flag_change_listener.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require 'http'
require 'json'

#
# A listener that receives FlagChange events and POSTs notifications to a callback URI.
# Implements the #update method expected by the SDK's FlagTracker.
#
class FlagChangeCallbackListener
def initialize(listener_id, callback_uri)
@listener_id = listener_id
@callback_uri = callback_uri
end

# @param flag_change [LaunchDarkly::Interfaces::FlagChange]
def update(flag_change)
payload = {
listenerId: @listener_id,
flagKey: flag_change.key,
}
HTTP.post(@callback_uri, json: payload)
rescue => e
# Log but don't re-raise; listener errors shouldn't crash the test service
$log.error("FlagChangeCallbackListener POST failed: #{e}")
end
end

#
# A listener that receives FlagValueChange events and POSTs notifications to a callback URI.
# Implements the #update method expected by the SDK's FlagTracker (via FlagValueChangeAdapter).
#
class FlagValueChangeCallbackListener
def initialize(listener_id, callback_uri)
@listener_id = listener_id
@callback_uri = callback_uri
end

# @param flag_value_change [LaunchDarkly::Interfaces::FlagValueChange]
def update(flag_value_change)
payload = {
listenerId: @listener_id,
flagKey: flag_value_change.key,
oldValue: flag_value_change.old_value,
newValue: flag_value_change.new_value,
}
HTTP.post(@callback_uri, json: payload)
rescue => e
$log.error("FlagValueChangeCallbackListener POST failed: #{e}")
end
end

#
# Manages all active flag change listener registrations for a single SDK client entity.
# Thread-safe via a Mutex.
#
class ListenerRegistry
# @param tracker [LaunchDarkly::Interfaces::FlagTracker]
def initialize(tracker)
@tracker = tracker
@mu = Mutex.new
@listeners = {} # listenerId => listener object to pass to remove_listener
end

# Registers a general flag change listener that fires on any flag configuration change.
#
# @param listener_id [String]
# @param callback_uri [String]
def register_flag_change_listener(listener_id, callback_uri)
listener = FlagChangeCallbackListener.new(listener_id, callback_uri)
@tracker.add_listener(listener)
store_listener(listener_id, listener)
end

# Registers a flag value change listener that fires when the evaluated value of a
# specific flag changes for a given context.
#
# @param listener_id [String]
# @param flag_key [String]
# @param context [LaunchDarkly::LDContext]
# @param callback_uri [String]
def register_flag_value_change_listener(listener_id, flag_key, context, callback_uri)
inner_listener = FlagValueChangeCallbackListener.new(listener_id, callback_uri)
# add_flag_value_change_listener returns the adapter object that must be passed to
# remove_listener for unregistration.
adapter = @tracker.add_flag_value_change_listener(flag_key, context, inner_listener)
store_listener(listener_id, adapter)
end

# Unregisters a previously registered listener by its ID.
#
# @param listener_id [String]
# @return [Boolean] true if the listener was found and removed
def unregister(listener_id)
listener = nil
@mu.synchronize do
listener = @listeners.delete(listener_id)
end

return false if listener.nil?

@tracker.remove_listener(listener)
true
end

# Removes all registered listeners. Called when the SDK client entity shuts down.
def close_all
listeners_to_remove = nil
@mu.synchronize do
listeners_to_remove = @listeners.values
@listeners = {}
end

listeners_to_remove.each do |listener|
@tracker.remove_listener(listener)
end
end

# Stores a listener, cancelling any previously registered listener with the same ID.
private def store_listener(listener_id, listener)
old_listener = nil
@mu.synchronize do
old_listener = @listeners[listener_id]
@listeners[listener_id] = listener
end

@tracker.remove_listener(old_listener) if old_listener
end
end
11 changes: 11 additions & 0 deletions contract-tests/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
'persistent-data-store-consul',
'persistent-data-store-dynamodb',
'persistent-data-store-redis',
'flag-change-listeners',
'flag-value-change-listeners',
],
}.to_json
end
Expand Down Expand Up @@ -128,6 +130,15 @@
when "contextComparison"
response = {:equals => client.context_comparison(params[:contextComparison])}
return [200, nil, response.to_json]
when "registerFlagChangeListener"
client.register_flag_change_listener(params[:registerFlagChangeListener])
return 201
when "registerFlagValueChangeListener"
client.register_flag_value_change_listener(params[:registerFlagValueChangeListener])
return 201
when "unregisterListener"
success = client.unregister_listener(params[:unregisterListener])
return success ? 201 : [400, nil, {:error => "no listener with that id"}.to_json]
end

return [400, nil, {:error => "Unknown command requested"}.to_json]
Expand Down