Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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)
store_listener(listener_id, listener)
@tracker.add_listener(listener)
end
Comment thread
cursor[bot] marked this conversation as resolved.

# 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