launchdarkly-server-sdk 8.11.2-java → 8.12.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +69 -9
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +227 -0
  5. data/lib/ldclient-rb/events.rb +34 -19
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +113 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +76 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
  21. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  22. data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
  23. data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
  24. data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
  25. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  26. data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
  27. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  28. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  29. data/lib/ldclient-rb/impl/event_sender.rb +14 -6
  30. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  32. data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +290 -0
  34. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  35. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  36. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  37. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  38. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  39. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  40. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  41. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
  42. data/lib/ldclient-rb/impl/util.rb +71 -0
  43. data/lib/ldclient-rb/impl.rb +1 -2
  44. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  45. data/lib/ldclient-rb/integrations/file_data.rb +67 -0
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  47. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  48. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  49. data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
  50. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  51. data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
  52. data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
  53. data/lib/ldclient-rb/ldclient.rb +66 -132
  54. data/lib/ldclient-rb/util.rb +11 -70
  55. data/lib/ldclient-rb/version.rb +1 -1
  56. data/lib/ldclient-rb.rb +9 -17
  57. metadata +41 -19
  58. data/lib/ldclient-rb/cache_store.rb +0 -45
  59. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  60. data/lib/ldclient-rb/memoized_value.rb +0 -32
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  62. data/lib/ldclient-rb/polling.rb +0 -102
  63. data/lib/ldclient-rb/requestor.rb +0 -102
  64. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  65. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,472 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require "ldclient-rb/config"
5
+ require "ldclient-rb/impl/data_system"
6
+ require "ldclient-rb/impl/data_store/store"
7
+ require "ldclient-rb/impl/data_store/feature_store_client_wrapper"
8
+ require "ldclient-rb/impl/data_source/status_provider"
9
+ require "ldclient-rb/impl/data_store/status_provider"
10
+ require "ldclient-rb/impl/broadcaster"
11
+ require "ldclient-rb/impl/repeating_task"
12
+ require "ldclient-rb/interfaces/data_system"
13
+
14
+ module LaunchDarkly
15
+ module Impl
16
+ module DataSystem
17
+ #
18
+ # Represents the possible outcomes from consuming synchronizer results.
19
+ #
20
+ # Used by {FDv2#consume_synchronizer_results} to indicate what action the
21
+ # synchronizer loop should take next.
22
+ #
23
+ module SyncResult
24
+ # Temporarily move to the next synchronizer in the list.
25
+ # The current synchronizer remains available for future recovery.
26
+ FALLBACK = :fallback
27
+
28
+ # Return to the first synchronizer in the list.
29
+ # Used when recovery conditions are met on a fallback synchronizer.
30
+ RECOVER = :recover
31
+
32
+ # Permanently remove the current synchronizer from the list.
33
+ # Used for unrecoverable failures (OFF state, exceptions).
34
+ REMOVE = :remove
35
+
36
+ # Switch to the FDv1 protocol fallback.
37
+ # Replaces the synchronizer list with the FDv1 fallback synchronizer.
38
+ FDV1 = :fdv1
39
+ end
40
+
41
+ # FDv2 is an implementation of the DataSystem interface that uses the Flag Delivery V2 protocol
42
+ # for obtaining and keeping data up-to-date. Additionally, it operates with an optional persistent
43
+ # store in read-only or read/write mode.
44
+ class FDv2
45
+ include LaunchDarkly::Impl::DataSystem
46
+
47
+ # Initialize a new FDv2 data system.
48
+ #
49
+ # @param sdk_key [String] The SDK key
50
+ # @param config [LaunchDarkly::Config] Configuration for initializers and synchronizers
51
+ # @param data_system_config [LaunchDarkly::DataSystemConfig] FDv2 data system configuration
52
+ def initialize(sdk_key, config, data_system_config)
53
+ @sdk_key = sdk_key
54
+ @config = config
55
+ @data_system_config = data_system_config
56
+ @logger = config.logger
57
+ @synchronizer_builders = data_system_config.synchronizers || []
58
+ @fdv1_fallback_synchronizer_builder = data_system_config.fdv1_fallback_synchronizer
59
+ @disabled = @config.offline?
60
+
61
+ # Diagnostic accumulator provided by client for streaming metrics
62
+ @diagnostic_accumulator = nil
63
+
64
+ # Shared executor for all broadcasters
65
+ @shared_executor = Concurrent::SingleThreadExecutor.new
66
+
67
+ # Set up event listeners
68
+ @flag_change_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
69
+ @change_set_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
70
+ @data_source_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
71
+ @data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
72
+
73
+ recovery_listener = Object.new
74
+ recovery_listener.define_singleton_method(:update) do |data_store_status|
75
+ persistent_store_outage_recovery(data_store_status)
76
+ end
77
+ @data_store_broadcaster.add_listener(recovery_listener)
78
+
79
+ # Create the store
80
+ @store = LaunchDarkly::Impl::DataStore::Store.new(@flag_change_broadcaster, @change_set_broadcaster, @logger)
81
+
82
+ # Status providers
83
+ @data_source_status_provider = LaunchDarkly::Impl::DataSource::StatusProviderV2.new(
84
+ @data_source_broadcaster
85
+ )
86
+ @data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProviderV2.new(nil, @data_store_broadcaster)
87
+
88
+ # Configure persistent store if provided
89
+ if @data_system_config.data_store
90
+ @data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProviderV2.new(
91
+ @data_system_config.data_store,
92
+ @data_store_broadcaster
93
+ )
94
+ writable = @data_system_config.data_store_mode == :read_write
95
+ wrapper = LaunchDarkly::Impl::DataStore::FeatureStoreClientWrapperV2.new(
96
+ @data_system_config.data_store,
97
+ @data_store_status_provider,
98
+ @logger
99
+ )
100
+ @store.with_persistence(wrapper, writable, @data_store_status_provider)
101
+ end
102
+
103
+ # Threading
104
+ @stop_event = Concurrent::Event.new
105
+ @ready_event = Concurrent::Event.new
106
+ @lock = Mutex.new
107
+ @active_synchronizer = nil
108
+ @threads = []
109
+
110
+ # Track configuration
111
+ @configured_with_data_sources = (@data_system_config.initializers && !@data_system_config.initializers.empty?) ||
112
+ !@synchronizer_builders.empty?
113
+ end
114
+
115
+ # (see DataSystem#start)
116
+ def start
117
+ if @disabled
118
+ @logger.warn { "[LDClient] Data system is disabled, SDK will return application-defined default values" }
119
+ @ready_event.set
120
+ return @ready_event
121
+ end
122
+
123
+ @stop_event.reset
124
+ @ready_event.reset
125
+
126
+ # Start the main coordination thread
127
+ main_thread = Thread.new { run_main_loop }
128
+ main_thread.name = "FDv2-main"
129
+ @threads << main_thread
130
+
131
+ @ready_event
132
+ end
133
+
134
+ # (see DataSystem#stop)
135
+ def stop
136
+ @stop_event.set
137
+
138
+ @lock.synchronize do
139
+ if @active_synchronizer
140
+ begin
141
+ @active_synchronizer.stop
142
+ rescue => e
143
+ @logger.error { "[LDClient] Error stopping active data source: #{e.message}" }
144
+ end
145
+ end
146
+ end
147
+
148
+ # Wait for all threads to complete
149
+ @threads.each do |thread|
150
+ next unless thread.alive?
151
+
152
+ thread.join(5.0) # 5 second timeout
153
+ @logger.warn { "[LDClient] Thread #{thread.name} did not terminate in time" } if thread.alive?
154
+ end
155
+
156
+ # Close the store
157
+ @store.close
158
+
159
+ # Shutdown the shared executor
160
+ @shared_executor.shutdown
161
+ end
162
+
163
+ # (see DataSystem#set_diagnostic_accumulator)
164
+ def set_diagnostic_accumulator(diagnostic_accumulator)
165
+ @diagnostic_accumulator = diagnostic_accumulator
166
+ end
167
+
168
+ # (see DataSystem#store)
169
+ def store
170
+ @store.get_active_store
171
+ end
172
+
173
+ # (see DataSystem#data_source_status_provider)
174
+ def data_source_status_provider
175
+ @data_source_status_provider
176
+ end
177
+
178
+ # (see DataSystem#data_store_status_provider)
179
+ def data_store_status_provider
180
+ @data_store_status_provider
181
+ end
182
+
183
+ # (see DataSystem#flag_change_broadcaster)
184
+ def flag_change_broadcaster
185
+ @flag_change_broadcaster
186
+ end
187
+
188
+ # (see DataSystem#data_availability)
189
+ def data_availability
190
+ return DataAvailability::REFRESHED if @store.selector.defined?
191
+ return DataAvailability::CACHED if !@configured_with_data_sources || @store.initialized?
192
+
193
+ DataAvailability::DEFAULTS
194
+ end
195
+
196
+ # (see DataSystem#target_availability)
197
+ def target_availability
198
+ return DataAvailability::REFRESHED if @configured_with_data_sources
199
+
200
+ DataAvailability::CACHED
201
+ end
202
+
203
+ private
204
+
205
+ #
206
+ # Main coordination loop that manages initializers and synchronizers.
207
+ #
208
+ # @return [void]
209
+ #
210
+ def run_main_loop
211
+ begin
212
+ @data_source_status_provider.update_status(
213
+ LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
214
+ nil
215
+ )
216
+
217
+ # Run initializers first
218
+ run_initializers
219
+
220
+ # Run synchronizers
221
+ run_synchronizers
222
+ rescue => e
223
+ @logger.error { "[LDClient] Error in FDv2 main loop: #{e.message}" }
224
+ @ready_event.set
225
+ end
226
+ end
227
+
228
+ #
229
+ # Run initializers to get initial data.
230
+ #
231
+ # @return [void]
232
+ #
233
+ def run_initializers
234
+ return unless @data_system_config.initializers
235
+
236
+ @data_system_config.initializers.each do |initializer_builder|
237
+ return if @stop_event.set?
238
+
239
+ begin
240
+ initializer = initializer_builder.build(@sdk_key, @config)
241
+ @logger.info { "[LDClient] Attempting to initialize via #{initializer.name}" }
242
+
243
+ basis_result = initializer.fetch(@store)
244
+
245
+ if basis_result.success?
246
+ basis = basis_result.value
247
+ @logger.info { "[LDClient] Initialized via #{initializer.name}" }
248
+
249
+ # Apply the basis to the store
250
+ @store.apply(basis.change_set, basis.persist)
251
+
252
+ # Set ready event if and only if a selector is defined for the changeset
253
+ if basis.change_set.selector && basis.change_set.selector.defined?
254
+ @ready_event.set
255
+ return
256
+ end
257
+ else
258
+ @logger.warn { "[LDClient] Initializer #{initializer.name} failed: #{basis_result.error}" }
259
+ end
260
+ rescue => e
261
+ @logger.error { "[LDClient] Initializer failed with exception: #{e.message}" }
262
+ end
263
+ end
264
+ end
265
+
266
+ #
267
+ # Run synchronizers to keep data up-to-date.
268
+ #
269
+ # @return [void]
270
+ #
271
+ def run_synchronizers
272
+ # If no synchronizers configured, just set ready and return
273
+ if @synchronizer_builders.empty?
274
+ @ready_event.set
275
+ return
276
+ end
277
+
278
+ # Start synchronizer loop in a separate thread
279
+ sync_thread = Thread.new { synchronizer_loop }
280
+ sync_thread.name = "FDv2-synchronizers"
281
+ @threads << sync_thread
282
+ end
283
+
284
+ #
285
+ # Synchronizer loop that manages synchronizers and fallbacks.
286
+ #
287
+ # @return [void]
288
+ #
289
+ def synchronizer_loop
290
+ # Track the current index in the synchronizer array
291
+ current_index = 0
292
+
293
+ begin
294
+ while !@stop_event.set? && current_index < @synchronizer_builders.length
295
+ synchronizer_builder = @synchronizer_builders[current_index]
296
+ is_primary = current_index == 0
297
+
298
+ begin
299
+ @lock.synchronize do
300
+ sync = synchronizer_builder.build(@sdk_key, @config)
301
+ if sync.respond_to?(:set_diagnostic_accumulator) && @diagnostic_accumulator
302
+ sync.set_diagnostic_accumulator(@diagnostic_accumulator)
303
+ end
304
+ @active_synchronizer = sync
305
+ end
306
+
307
+ @logger.info { "[LDClient] Synchronizer[#{current_index}] #{@active_synchronizer.name} is starting" }
308
+
309
+ sync_result = consume_synchronizer_results(@active_synchronizer, check_recovery: !is_primary)
310
+
311
+ break if @stop_event.set?
312
+
313
+ case sync_result
314
+ when SyncResult::FDV1
315
+ if @fdv1_fallback_synchronizer_builder
316
+ @synchronizer_builders = [@fdv1_fallback_synchronizer_builder]
317
+ current_index = 0
318
+ next
319
+ end
320
+ # No FDv1 fallback configured, treat as regular fallback
321
+ current_index += 1
322
+ when SyncResult::RECOVER
323
+ @logger.info { "[LDClient] Recovery condition met, returning to primary synchronizer" }
324
+ current_index = 0
325
+ when SyncResult::REMOVE
326
+ @logger.info { "[LDClient] Removing synchronizer from list due to permanent failure" }
327
+ @synchronizer_builders.delete_at(current_index)
328
+ else
329
+ @logger.info { "[LDClient] Fallback condition met" }
330
+ current_index += 1
331
+ end
332
+
333
+ current_index = 0 if current_index >= @synchronizer_builders.length
334
+
335
+ if @synchronizer_builders.length == 0
336
+ @logger.warn { "[LDClient] No more synchronizers available" }
337
+ @data_source_status_provider.update_status(
338
+ LaunchDarkly::Interfaces::DataSource::Status::OFF,
339
+ @data_source_status_provider.status.last_error
340
+ )
341
+ break
342
+ end
343
+ rescue => e
344
+ @logger.error { "[LDClient] Failed to build synchronizer: #{e.message}" }
345
+ break
346
+ end
347
+ end
348
+ rescue => e
349
+ @logger.error { "[LDClient] Error in synchronizer loop: #{e.message}" }
350
+ ensure
351
+ # Ensure we always set the ready event when exiting
352
+ @ready_event.set
353
+ @lock.synchronize do
354
+ @active_synchronizer&.stop
355
+ @active_synchronizer = nil
356
+ end
357
+ end
358
+ end
359
+
360
+ #
361
+ # Consume results from a synchronizer until a condition is met or it fails.
362
+ #
363
+ # @param synchronizer [Object] The synchronizer
364
+ # @param check_recovery [Boolean] Whether to check recovery condition (healthy too long)
365
+ # @return [Symbol] One of {SyncResult::FALLBACK}, {SyncResult::RECOVER}, {SyncResult::REMOVE}, or {SyncResult::FDV1}
366
+ #
367
+ def consume_synchronizer_results(synchronizer, check_recovery: false)
368
+ action_queue = Queue.new
369
+ timer = LaunchDarkly::Impl::RepeatingTask.new(10, 10, -> { action_queue.push("check") }, @logger, "FDv2-sync-cond-timer")
370
+
371
+ # Start reader thread
372
+ sync_reader = Thread.new do
373
+ begin
374
+ synchronizer.sync(@store) do |update|
375
+ action_queue.push(update)
376
+ end
377
+ ensure
378
+ action_queue.push("quit")
379
+ end
380
+ end
381
+ sync_reader.name = "FDv2-sync-reader"
382
+
383
+ begin
384
+ timer.start
385
+
386
+ loop do
387
+ update = action_queue.pop
388
+
389
+ if update.is_a?(String)
390
+ break if update == "quit"
391
+
392
+ if update == "check"
393
+ # Check condition periodically
394
+ current_status = @data_source_status_provider.status
395
+ return SyncResult::RECOVER if check_recovery && recovery_condition(current_status)
396
+ return SyncResult::FALLBACK if fallback_condition(current_status)
397
+ end
398
+ next
399
+ end
400
+
401
+ @logger.info { "[LDClient] Synchronizer #{synchronizer.name} update: #{update.state}" }
402
+ return SyncResult::FALLBACK if @stop_event.set?
403
+
404
+ # Handle the update
405
+ @store.apply(update.change_set, true) if update.change_set
406
+
407
+ # Set ready event on valid update
408
+ @ready_event.set if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
409
+
410
+ # Update status
411
+ @data_source_status_provider.update_status(update.state, update.error)
412
+
413
+ return SyncResult::FDV1 if update.revert_to_fdv1
414
+
415
+ return SyncResult::REMOVE if update.state == LaunchDarkly::Interfaces::DataSource::Status::OFF
416
+ end
417
+ rescue => e
418
+ @logger.error { "[LDClient] Error consuming synchronizer results: #{e.message}" }
419
+ return SyncResult::REMOVE
420
+ ensure
421
+ synchronizer.stop
422
+ timer.stop
423
+ sync_reader.join(0.5) if sync_reader.alive?
424
+ end
425
+
426
+ SyncResult::REMOVE
427
+ end
428
+
429
+ #
430
+ # Determine if we should fallback to the next synchronizer.
431
+ #
432
+ # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
433
+ # @return [Boolean] true if fallback condition is met
434
+ #
435
+ def fallback_condition(status)
436
+ interrupted_at_runtime = status.state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
437
+ Time.now - status.state_since > 60 # 1 minute
438
+ cannot_initialize = status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING &&
439
+ Time.now - status.state_since > 10 # 10 seconds
440
+
441
+ interrupted_at_runtime || cannot_initialize
442
+ end
443
+
444
+ #
445
+ # Determine if we should recover to the primary synchronizer.
446
+ #
447
+ # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
448
+ # @return [Boolean] true if recovery condition is met (healthy for too long)
449
+ #
450
+ def recovery_condition(status)
451
+ status.state == LaunchDarkly::Interfaces::DataSource::Status::VALID &&
452
+ Time.now - status.state_since > 300 # 5 minutes
453
+ end
454
+
455
+ #
456
+ # Monitor the data store status. If the store comes online and
457
+ # potentially has stale data, we should write our known state to it.
458
+ #
459
+ # @param data_store_status [LaunchDarkly::Interfaces::DataStore::Status] The store status
460
+ # @return [void]
461
+ #
462
+ def persistent_store_outage_recovery(data_store_status)
463
+ return unless data_store_status.available
464
+ return unless data_store_status.stale
465
+
466
+ err = @store.commit
467
+ @logger.error { "[LDClient] Failed to reinitialize data store: #{err.message}" } if err
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module DataSystem
6
+ #
7
+ # HttpConfigOptions contains HTTP connection configuration settings.
8
+ # This class is created by data source builders and passed to v2 Requesters/DataSources.
9
+ #
10
+ class HttpConfigOptions
11
+ # Generic HTTP defaults - base URIs live in the respective builders
12
+ DEFAULT_READ_TIMEOUT = 10
13
+ DEFAULT_CONNECT_TIMEOUT = 2
14
+
15
+ attr_reader :base_uri, :socket_factory, :read_timeout, :connect_timeout
16
+
17
+ #
18
+ # @param base_uri [String] The base URI for HTTP requests
19
+ # @param socket_factory [Object, nil] Optional socket factory for custom connections
20
+ # @param read_timeout [Float, nil] Read timeout in seconds (defaults to DEFAULT_READ_TIMEOUT)
21
+ # @param connect_timeout [Float, nil] Connect timeout in seconds (defaults to DEFAULT_CONNECT_TIMEOUT)
22
+ #
23
+ def initialize(base_uri:, socket_factory: nil, read_timeout: nil, connect_timeout: nil)
24
+ @base_uri = base_uri
25
+ @socket_factory = socket_factory
26
+ @read_timeout = read_timeout || DEFAULT_READ_TIMEOUT
27
+ @connect_timeout = connect_timeout || DEFAULT_CONNECT_TIMEOUT
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end