Skip to content

Commit 48f444e

Browse files
chore: Support flag change listeners in contract tests (#368)
1 parent 6e99e14 commit 48f444e

File tree

4 files changed

+161
-1
lines changed

4 files changed

+161
-1
lines changed

.github/actions/check/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ runs:
5151
test_service_port: 9000
5252
enable_persistence_tests: true
5353
token: ${{ inputs.token }}
54-
version: v3.0.0-alpha.3
54+
version: v3.0.0-alpha.4

contract-tests/client_entity.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'net/http'
44
require 'launchdarkly-server-sdk'
55
require './big_segment_store_fixture'
6+
require './flag_change_listener'
67
require './hook'
78
require 'http'
89

@@ -117,6 +118,8 @@ def initialize(log, config)
117118
config[:credential],
118119
LaunchDarkly::Config.new(opts),
119120
startWaitTimeMs / 1_000.0)
121+
122+
@listeners = ListenerRegistry.new(@client.flag_tracker)
120123
end
121124

122125
def initialized?
@@ -225,7 +228,26 @@ def log
225228
@log
226229
end
227230

231+
def register_flag_change_listener(params)
232+
@listeners.register_flag_change_listener(params[:listenerId], params[:callbackUri])
233+
end
234+
235+
def register_flag_value_change_listener(params)
236+
context = LaunchDarkly::LDContext.create(params[:context])
237+
@listeners.register_flag_value_change_listener(
238+
params[:listenerId],
239+
params[:flagKey],
240+
context,
241+
params[:callbackUri]
242+
)
243+
end
244+
245+
def unregister_listener(params)
246+
@listeners.unregister(params[:listenerId])
247+
end
248+
228249
def close
250+
@listeners.close_all
229251
@client.close
230252
@log.info("Test ended")
231253
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
require 'http'
2+
require 'json'
3+
4+
#
5+
# A listener that receives FlagChange events and POSTs notifications to a callback URI.
6+
# Implements the #update method expected by the SDK's FlagTracker.
7+
#
8+
class FlagChangeCallbackListener
9+
def initialize(listener_id, callback_uri)
10+
@listener_id = listener_id
11+
@callback_uri = callback_uri
12+
end
13+
14+
# @param flag_change [LaunchDarkly::Interfaces::FlagChange]
15+
def update(flag_change)
16+
payload = {
17+
listenerId: @listener_id,
18+
flagKey: flag_change.key,
19+
}
20+
HTTP.post(@callback_uri, json: payload)
21+
rescue => e
22+
# Log but don't re-raise; listener errors shouldn't crash the test service
23+
$log.error("FlagChangeCallbackListener POST failed: #{e}")
24+
end
25+
end
26+
27+
#
28+
# A listener that receives FlagValueChange events and POSTs notifications to a callback URI.
29+
# Implements the #update method expected by the SDK's FlagTracker (via FlagValueChangeAdapter).
30+
#
31+
class FlagValueChangeCallbackListener
32+
def initialize(listener_id, callback_uri)
33+
@listener_id = listener_id
34+
@callback_uri = callback_uri
35+
end
36+
37+
# @param flag_value_change [LaunchDarkly::Interfaces::FlagValueChange]
38+
def update(flag_value_change)
39+
payload = {
40+
listenerId: @listener_id,
41+
flagKey: flag_value_change.key,
42+
oldValue: flag_value_change.old_value,
43+
newValue: flag_value_change.new_value,
44+
}
45+
HTTP.post(@callback_uri, json: payload)
46+
rescue => e
47+
$log.error("FlagValueChangeCallbackListener POST failed: #{e}")
48+
end
49+
end
50+
51+
#
52+
# Manages all active flag change listener registrations for a single SDK client entity.
53+
# Thread-safe via a Mutex.
54+
#
55+
class ListenerRegistry
56+
# @param tracker [LaunchDarkly::Interfaces::FlagTracker]
57+
def initialize(tracker)
58+
@tracker = tracker
59+
@mu = Mutex.new
60+
@listeners = {} # listenerId => listener object to pass to remove_listener
61+
end
62+
63+
# Registers a general flag change listener that fires on any flag configuration change.
64+
#
65+
# @param listener_id [String]
66+
# @param callback_uri [String]
67+
def register_flag_change_listener(listener_id, callback_uri)
68+
listener = FlagChangeCallbackListener.new(listener_id, callback_uri)
69+
@tracker.add_listener(listener)
70+
store_listener(listener_id, listener)
71+
end
72+
73+
# Registers a flag value change listener that fires when the evaluated value of a
74+
# specific flag changes for a given context.
75+
#
76+
# @param listener_id [String]
77+
# @param flag_key [String]
78+
# @param context [LaunchDarkly::LDContext]
79+
# @param callback_uri [String]
80+
def register_flag_value_change_listener(listener_id, flag_key, context, callback_uri)
81+
inner_listener = FlagValueChangeCallbackListener.new(listener_id, callback_uri)
82+
# add_flag_value_change_listener returns the adapter object that must be passed to
83+
# remove_listener for unregistration.
84+
adapter = @tracker.add_flag_value_change_listener(flag_key, context, inner_listener)
85+
store_listener(listener_id, adapter)
86+
end
87+
88+
# Unregisters a previously registered listener by its ID.
89+
#
90+
# @param listener_id [String]
91+
# @return [Boolean] true if the listener was found and removed
92+
def unregister(listener_id)
93+
listener = nil
94+
@mu.synchronize do
95+
listener = @listeners.delete(listener_id)
96+
end
97+
98+
return false if listener.nil?
99+
100+
@tracker.remove_listener(listener)
101+
true
102+
end
103+
104+
# Removes all registered listeners. Called when the SDK client entity shuts down.
105+
def close_all
106+
listeners_to_remove = nil
107+
@mu.synchronize do
108+
listeners_to_remove = @listeners.values
109+
@listeners = {}
110+
end
111+
112+
listeners_to_remove.each do |listener|
113+
@tracker.remove_listener(listener)
114+
end
115+
end
116+
117+
# Stores a listener, cancelling any previously registered listener with the same ID.
118+
private def store_listener(listener_id, listener)
119+
old_listener = nil
120+
@mu.synchronize do
121+
old_listener = @listeners[listener_id]
122+
@listeners[listener_id] = listener
123+
end
124+
125+
@tracker.remove_listener(old_listener) if old_listener
126+
end
127+
end

contract-tests/service.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
'persistent-data-store-consul',
5252
'persistent-data-store-dynamodb',
5353
'persistent-data-store-redis',
54+
'flag-change-listeners',
55+
'flag-value-change-listeners',
5456
],
5557
}.to_json
5658
end
@@ -128,6 +130,15 @@
128130
when "contextComparison"
129131
response = {:equals => client.context_comparison(params[:contextComparison])}
130132
return [200, nil, response.to_json]
133+
when "registerFlagChangeListener"
134+
client.register_flag_change_listener(params[:registerFlagChangeListener])
135+
return 201
136+
when "registerFlagValueChangeListener"
137+
client.register_flag_value_change_listener(params[:registerFlagValueChangeListener])
138+
return 201
139+
when "unregisterListener"
140+
success = client.unregister_listener(params[:unregisterListener])
141+
return success ? 201 : [400, nil, {:error => "no listener with that id"}.to_json]
131142
end
132143

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

0 commit comments

Comments
 (0)