Skip to content

Commit bdc5413

Browse files
committed
feat: Add posthook method to re-initialize after forking
1 parent 39b1e0e commit bdc5413

File tree

5 files changed

+62
-9
lines changed

5 files changed

+62
-9
lines changed

lib/ldclient-rb/config.rb

+13
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def initialize(opts = {})
8181
@hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
8282
@omit_anonymous_contexts = opts.has_key?(:omit_anonymous_contexts) && opts[:omit_anonymous_contexts]
8383
@data_source_update_sink = nil
84+
@instance_id = nil
8485
end
8586

8687
#
@@ -97,6 +98,18 @@ def initialize(opts = {})
9798
#
9899
attr_accessor :data_source_update_sink
99100

101+
102+
#
103+
# Returns the unique identifier for this instance of the SDK.
104+
#
105+
# This property should only be set by the SDK. Long term access of this
106+
# property is not supported; it is temporarily being exposed to maintain
107+
# backwards compatibility while the SDK structure is updated.
108+
#
109+
# @private
110+
#
111+
attr_accessor :instance_id
112+
100113
#
101114
# The base URL for the LaunchDarkly server. This is configurable mainly for testing
102115
# purposes; most users should use the default value.

lib/ldclient-rb/impl/util.rb

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ def self.current_time_millis
1111

1212
def self.default_http_headers(sdk_key, config)
1313
ret = { "Authorization" => sdk_key, "User-Agent" => "RubyClient/" + LaunchDarkly::VERSION }
14+
15+
ret["X-LaunchDarkly-Instance-Id"] = config.instance_id unless config.instance_id.nil?
16+
1417
if config.wrapper_name
1518
ret["X-LaunchDarkly-Wrapper"] = config.wrapper_name +
1619
(config.wrapper_version ? "/" + config.wrapper_version : "")

lib/ldclient-rb/ldclient.rb

+42-9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require "digest/sha1"
1414
require "forwardable"
1515
require "logger"
16+
require "securerandom"
1617
require "benchmark"
1718
require "json"
1819
require "openssl"
@@ -56,25 +57,57 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
5657
end
5758

5859
@sdk_key = sdk_key
59-
@hooks = Concurrent::Array.new(config.hooks)
60+
config.instance_id = SecureRandom.uuid
61+
@config = config
62+
63+
start_up(wait_for_sec)
64+
end
65+
66+
#
67+
# Re-initializes an existing client after a process fork.
68+
#
69+
# The SDK relies on multiple background threads to operate correctly. When a process forks, `these threads are not
70+
# available to the child <https://apidock.com/ruby/Process/fork/class>`.
71+
#
72+
# As a result, the SDK will not function correctly in the child process until it is re-initialized.
73+
#
74+
# This method is effectively equivalent to instantiating a new client. Future iterations of the SDK will provide
75+
# increasingly efficient re-initializing improvements.
76+
#
77+
# Note that any configuration provided to the SDK will need to survive the forking process independently. For this
78+
# reason, it is recommended that any listener or hook integrations be added postfork unless you are certain it can
79+
# survive the forking process.
80+
#
81+
# @param wait_for_sec [Float] maximum time (in seconds) to wait for initialization
82+
#
83+
def postfork(wait_for_sec = 5)
84+
@data_source = nil
85+
@event_processor = nil
86+
@big_segment_store_manager = nil
87+
88+
start_up(wait_for_sec)
89+
end
90+
91+
private def start_up(wait_for_sec)
92+
@hooks = Concurrent::Array.new(@config.hooks)
6093

6194
@shared_executor = Concurrent::SingleThreadExecutor.new
6295

63-
data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, config.logger)
96+
data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger)
6497
store_sink = LaunchDarkly::Impl::DataStore::UpdateSink.new(data_store_broadcaster)
6598

6699
# We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
67100
# some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
68101
# the feature store through the Config object, so we need to make a new Config that uses
69102
# the wrapped store.
70-
@store = Impl::FeatureStoreClientWrapper.new(config.feature_store, store_sink, config.logger)
71-
updated_config = config.clone
103+
@store = Impl::FeatureStoreClientWrapper.new(@config.feature_store, store_sink, @config.logger)
104+
updated_config = @config.clone
72105
updated_config.instance_variable_set(:@feature_store, @store)
73106
@config = updated_config
74107

75108
@data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProvider.new(@store, store_sink)
76109

77-
@big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
110+
@big_segment_store_manager = Impl::BigSegmentStoreManager.new(@config.big_segments, @config.logger)
78111
@big_segment_store_status_provider = @big_segment_store_manager.status_provider
79112

80113
get_flag = lambda { |key| @store.get(FEATURES, key) }
@@ -83,15 +116,15 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
83116
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
84117

85118
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
86-
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
119+
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(@sdk_key))
87120
else
88121
diagnostic_accumulator = nil
89122
end
90123

91124
if @config.offline? || !@config.send_events
92125
@event_processor = NullEventProcessor.new
93126
else
94-
@event_processor = EventProcessor.new(sdk_key, config, nil, diagnostic_accumulator)
127+
@event_processor = EventProcessor.new(@sdk_key, @config, nil, diagnostic_accumulator)
95128
end
96129

97130
if @config.use_ldd?
@@ -115,9 +148,9 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
115148
# Currently, data source factories take two parameters unless they need to be aware of diagnostic_accumulator, in
116149
# which case they take three parameters. This will be changed in the future to use a less awkware mechanism.
117150
if data_source_or_factory.arity == 3
118-
@data_source = data_source_or_factory.call(sdk_key, @config, diagnostic_accumulator)
151+
@data_source = data_source_or_factory.call(@sdk_key, @config, diagnostic_accumulator)
119152
else
120-
@data_source = data_source_or_factory.call(sdk_key, @config)
153+
@data_source = data_source_or_factory.call(@sdk_key, @config)
121154
end
122155
else
123156
@data_source = data_source_or_factory

spec/impl/event_sender_spec.rb

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def with_sender_and_server(config_options = {})
2929
it "sends analytics event data without compression enabled" do
3030
with_sender_and_server(compress_events: false) do |es, server|
3131
server.setup_ok_response("/bulk", "")
32+
es.instance_variable_get(:@config).instance_id = 'instance-id'
3233

3334
result = es.send_event_data(fake_data, "", false)
3435

@@ -43,6 +44,7 @@ def with_sender_and_server(config_options = {})
4344
"content-type" => [ "application/json" ],
4445
"user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
4546
"x-launchdarkly-event-schema" => [ "4" ],
47+
"x-launchdarkly-instance-id" => [ "instance-id" ],
4648
"connection" => [ "Keep-Alive" ],
4749
})
4850
expect(req.header['x-launchdarkly-payload-id']).not_to eq []

spec/requestor_spec.rb

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def with_requestor(base_uri, opts = {})
1919
it "uses expected URI and headers" do
2020
with_server do |server|
2121
with_requestor(server.base_uri.to_s) do |requestor|
22+
requestor.instance_variable_get(:@config).instance_id = 'instance-id'
2223
server.setup_ok_response("/", "{}")
2324
requestor.request_all_data
2425
expect(server.requests.count).to eq 1
@@ -27,6 +28,7 @@ def with_requestor(base_uri, opts = {})
2728
"authorization" => [ sdk_key ],
2829
"user-agent" => [ "RubyClient/" + VERSION ],
2930
"x-launchdarkly-tags" => [ "application-id/id application-version/version" ],
31+
"x-launchdarkly-instance-id" => [ "instance-id"],
3032
})
3133
end
3234
end

0 commit comments

Comments
 (0)