ldclient-rb 5.5.2 → 5.5.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7d93c2f80940a876df8208c9ab53de33b9489cb1
4
- data.tar.gz: f79f9ba1ac7e7716f14d20f8a6a83d5d8027c840
3
+ metadata.gz: 8e5398b81dc216177803939d535b130edbb0361a
4
+ data.tar.gz: 4c50f29da29da753db0a36064c29a22191960808
5
5
  SHA512:
6
- metadata.gz: 4151fd38371a01e1856c591d50a5f13e5818488266247e73f25a13273982e06bb47f35ca09cec46cb1389737acaf39818325fcd1a56138a0e5fcd66d4a30f8f2
7
- data.tar.gz: acec0d1362df8a46fa7077423202462866bec5af12e7d86f27c1b9d18c76ccc6c4d89572caa44258c57df4ac5bc513ff19d1347cdd8598f29c85f90b6d0896c0
6
+ metadata.gz: 894d4f29200b6a5df70608942f15223a9b9d5bdeb426913f3afe9feb7d23b02b2ef544874ba7c39570b3f8e8c098caafab638ff9b779b9aec69ca444d1115b62
7
+ data.tar.gz: 41c088debbc36d16b7518b20af0ddc4c9b488e0da16b9e465d573ab6e715f00612fb53ee4fe492692cf0d065aa16c3e85ba2d17314c1c326110415ac4667b310
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
+ ## [5.5.3] - 2019-02-13
6
+ ### Changed:
7
+ - The SDK previously used the `faraday` and `net-http-persistent` gems for all HTTP requests other than streaming connections. Since `faraday` lacks a stable version and has a known issue with character encoding, and `net-http-persistent` is no longer maintained, these have both been removed. This should not affect any SDK functionality.
8
+
9
+ ### Fixed:
10
+ - The SDK was not usable in Windows because of `net-http-persistent`. That gem has been removed.
11
+ - When running in Windows, the event-processing thread threw a `RangeError` due to a difference in the Windows implementation of `concurrent-ruby`. This has been fixed.
12
+ - Windows incompatibilities were undetected before because we were not running a Windows CI job. We are now testing on Windows with Ruby 2.5.
13
+
5
14
  ## [5.5.2] - 2019-01-18
6
15
  ### Fixed:
7
16
  - Like 5.5.1, this release contains only documentation fixes. Implementation classes that are not part of the supported API are now hidden from the [generated documentation](https://www.rubydoc.info/gems/ldclient-rb).
@@ -1,13 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ldclient-rb (5.4.3)
4
+ ldclient-rb (5.5.2)
5
5
  concurrent-ruby (~> 1.0)
6
- faraday (>= 0.9, < 2)
7
- faraday-http-cache (>= 1.3.0, < 3)
8
6
  json (>= 1.8, < 3)
9
7
  ld-eventsource (~> 1.0)
10
- net-http-persistent (>= 2.9, < 4.0)
11
8
  semantic (~> 1.6)
12
9
 
13
10
  GEM
@@ -35,11 +32,10 @@ GEM
35
32
  docile (1.1.5)
36
33
  faraday (0.15.4)
37
34
  multipart-post (>= 1.2, < 3)
38
- faraday-http-cache (2.0.0)
39
- faraday (~> 0.8)
40
35
  ffi (1.9.25)
41
36
  ffi (1.9.25-java)
42
- hitimes (1.3.0)
37
+ hitimes (1.3.1)
38
+ hitimes (1.3.1-java)
43
39
  http_tools (0.4.5)
44
40
  jmespath (1.4.0)
45
41
  json (1.8.6)
@@ -53,8 +49,6 @@ GEM
53
49
  rb-inotify (~> 0.9, >= 0.9.7)
54
50
  ruby_dep (~> 1.2)
55
51
  multipart-post (2.0.0)
56
- net-http-persistent (3.0.0)
57
- connection_pool (~> 2.2)
58
52
  rake (10.5.0)
59
53
  rb-fsevent (0.10.3)
60
54
  rb-inotify (0.9.10)
data/README.md CHANGED
@@ -17,19 +17,19 @@ Quick setup
17
17
 
18
18
  1. Install the Ruby SDK with `gem`
19
19
 
20
- ```shell
20
+ ```shell
21
21
  gem install ldclient-rb
22
22
  ```
23
23
 
24
24
  2. Require the LaunchDarkly client:
25
25
 
26
- ```ruby
26
+ ```ruby
27
27
  require 'ldclient-rb'
28
28
  ```
29
29
 
30
30
  3. Create a new LDClient with your SDK key:
31
31
 
32
- ```ruby
32
+ ```ruby
33
33
  client = LaunchDarkly::LDClient.new("your_sdk_key")
34
34
  ```
35
35
 
@@ -39,42 +39,42 @@ client = LaunchDarkly::LDClient.new("your_sdk_key")
39
39
 
40
40
  2. Initialize the launchdarkly client in `config/initializers/launchdarkly.rb`:
41
41
 
42
- ```ruby
42
+ ```ruby
43
43
  Rails.configuration.ld_client = LaunchDarkly::LDClient.new("your_sdk_key")
44
44
  ```
45
45
 
46
46
  3. You may want to include a function in your ApplicationController
47
47
 
48
- ```ruby
49
- def launchdarkly_settings
50
- if current_user.present?
51
- {
52
- key: current_user.id,
53
- anonymous: false,
54
- email: current_user.email,
55
- custom: { groups: current_user.groups.pluck(:name) },
56
- # Any other fields you may have
57
- # e.g. lastName: current_user.last_name,
58
- }
59
- else
60
- if Rails::VERSION::MAJOR <= 3
61
- hash_key = request.session_options[:id]
62
- else
63
- hash_key = session.id
64
- end
65
- # session ids should be private to prevent session hijacking
66
- hash_key = Digest::SHA256.base64digest hash_key
67
- {
68
- key: hash_key,
69
- anonymous: true,
70
- }
71
- end
48
+ ```ruby
49
+ def launchdarkly_settings
50
+ if current_user.present?
51
+ {
52
+ key: current_user.id,
53
+ anonymous: false,
54
+ email: current_user.email,
55
+ custom: { groups: current_user.groups.pluck(:name) },
56
+ # Any other fields you may have
57
+ # e.g. lastName: current_user.last_name,
58
+ }
59
+ else
60
+ if Rails::VERSION::MAJOR <= 3
61
+ hash_key = request.session_options[:id]
62
+ else
63
+ hash_key = session.id
72
64
  end
65
+ # session ids should be private to prevent session hijacking
66
+ hash_key = Digest::SHA256.base64digest hash_key
67
+ {
68
+ key: hash_key,
69
+ anonymous: true,
70
+ }
71
+ end
72
+ end
73
73
  ```
74
74
 
75
75
  4. In your controllers, access the client using
76
76
 
77
- ```ruby
77
+ ```ruby
78
78
  Rails.application.config.ld_client.variation('your.flag.key', launchdarkly_settings, false)
79
79
  ```
80
80
 
@@ -0,0 +1,51 @@
1
+ jobs:
2
+ - job: build
3
+ pool:
4
+ vmImage: 'vs2017-win2016'
5
+ steps:
6
+ - task: PowerShell@2
7
+ displayName: 'Setup Dynamo'
8
+ inputs:
9
+ targetType: inline
10
+ workingDirectory: $(System.DefaultWorkingDirectory)
11
+ script: |
12
+ iwr -outf dynamo.zip https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.zip
13
+ mkdir dynamo
14
+ Expand-Archive -Path dynamo.zip -DestinationPath dynamo
15
+ cd dynamo
16
+ javaw -D"java.library.path=./DynamoDBLocal_lib" -jar DynamoDBLocal.jar
17
+ - task: PowerShell@2
18
+ displayName: 'Setup Consul'
19
+ inputs:
20
+ targetType: inline
21
+ workingDirectory: $(System.DefaultWorkingDirectory)
22
+ script: |
23
+ iwr -outf consul.zip https://releases.hashicorp.com/consul/1.4.2/consul_1.4.2_windows_amd64.zip
24
+ mkdir consul
25
+ Expand-Archive -Path consul.zip -DestinationPath consul
26
+ cd consul
27
+ sc.exe create "Consul" binPath="$(System.DefaultWorkingDirectory)/consul/consul.exe agent -dev"
28
+ sc.exe start "Consul"
29
+ - task: PowerShell@2
30
+ displayName: 'Setup Redis'
31
+ inputs:
32
+ targetType: inline
33
+ workingDirectory: $(System.DefaultWorkingDirectory)
34
+ script: |
35
+ iwr -outf redis.zip https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.zip
36
+ mkdir redis
37
+ Expand-Archive -Path redis.zip -DestinationPath redis
38
+ cd redis
39
+ ./redis-server --service-install
40
+ ./redis-server --service-start
41
+ - task: PowerShell@2
42
+ displayName: 'Setup SDK and Test'
43
+ inputs:
44
+ targetType: inline
45
+ workingDirectory: $(System.DefaultWorkingDirectory)
46
+ script: |
47
+ ruby -v
48
+ gem install bundler -v 1.17.3
49
+ bundle install
50
+ mkdir rspec
51
+ bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
@@ -34,10 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency "listen", "~> 3.0" # see file_data_source.rb
35
35
 
36
36
  spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
37
- spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
38
- spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
39
37
  spec.add_runtime_dependency "semantic", "~> 1.6"
40
- spec.add_runtime_dependency "net-http-persistent", [">= 2.9", "< 4.0"]
41
38
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
42
39
  spec.add_runtime_dependency "ld-eventsource", '~> 1.0'
43
40
  end
@@ -2,11 +2,9 @@ require "concurrent/map"
2
2
 
3
3
  module LaunchDarkly
4
4
  #
5
- # A thread-safe in-memory store suitable for use with the Faraday caching HTTP client. Uses the
6
- # concurrent-ruby gem's Map as the underlying cache.
5
+ # A thread-safe in-memory store that uses the same semantics that Faraday would expect, although we
6
+ # no longer use Faraday. This is used by Requestor, when we are not in a Rails environment.
7
7
  #
8
- # @see https://github.com/plataformatec/faraday-http-cache
9
- # @see https://github.com/ruby-concurrency
10
8
  # @private
11
9
  #
12
10
  class ThreadSafeMemoryStore
@@ -53,7 +53,6 @@ module LaunchDarkly
53
53
  @use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
54
54
  @offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
55
55
  @poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval
56
- @proxy = opts[:proxy] || Config.default_proxy
57
56
  @all_attributes_private = opts[:all_attributes_private] || false
58
57
  @private_attribute_names = opts[:private_attribute_names] || []
59
58
  @send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
@@ -153,9 +152,10 @@ module LaunchDarkly
153
152
  attr_reader :capacity
154
153
 
155
154
  #
156
- # A store for HTTP caching. This must support the semantics used by the
157
- # [`faraday-http-cache`](https://github.com/plataformatec/faraday-http-cache) gem. Defaults
158
- # to the Rails cache in a Rails environment, or a thread-safe in-memory store otherwise.
155
+ # A store for HTTP caching (used only in polling mode). This must support the semantics used by
156
+ # the [`faraday-http-cache`](https://github.com/plataformatec/faraday-http-cache) gem, although
157
+ # the SDK no longer uses Faraday. Defaults to the Rails cache in a Rails environment, or a
158
+ # thread-safe in-memory store otherwise.
159
159
  # @return [Object]
160
160
  #
161
161
  attr_reader :cache_store
@@ -184,12 +184,6 @@ module LaunchDarkly
184
184
  #
185
185
  attr_reader :feature_store
186
186
 
187
- #
188
- # The proxy configuration string.
189
- # @return [String]
190
- #
191
- attr_reader :proxy
192
-
193
187
  #
194
188
  # True if all user attributes (other than the key) should be considered private. This means
195
189
  # that the attribute values will not be sent to LaunchDarkly in analytics events and will not
@@ -336,14 +330,6 @@ module LaunchDarkly
336
330
  2
337
331
  end
338
332
 
339
- #
340
- # The default value for {#proxy}.
341
- # @return [String] nil
342
- #
343
- def self.default_proxy
344
- nil
345
- end
346
-
347
333
  #
348
334
  # The default value for {#logger}.
349
335
  # @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise
@@ -3,7 +3,6 @@ require "concurrent/atomics"
3
3
  require "concurrent/executors"
4
4
  require "thread"
5
5
  require "time"
6
- require "faraday"
7
6
 
8
7
  module LaunchDarkly
9
8
  MAX_FLUSH_WORKERS = 5
@@ -115,11 +114,17 @@ module LaunchDarkly
115
114
  def initialize(queue, sdk_key, config, client)
116
115
  @sdk_key = sdk_key
117
116
  @config = config
118
- @client = client ? client : Faraday.new
117
+
118
+ if client
119
+ @client = client
120
+ else
121
+ @client = Util.new_http_client(@config.events_uri, @config)
122
+ end
123
+
119
124
  @user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
120
125
  @formatter = EventOutputFormatter.new(config)
121
126
  @disabled = Concurrent::AtomicBoolean.new(false)
122
- @last_known_past_time = Concurrent::AtomicFixnum.new(0)
127
+ @last_known_past_time = Concurrent::AtomicReference.new(0)
123
128
 
124
129
  buffer = EventBuffer.new(config.capacity, config.logger)
125
130
  flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
@@ -162,7 +167,10 @@ module LaunchDarkly
162
167
  def do_shutdown(flush_workers)
163
168
  flush_workers.shutdown
164
169
  flush_workers.wait_for_termination
165
- # There seems to be no such thing as "close" in Faraday: https://github.com/lostisland/faraday/issues/241
170
+ begin
171
+ @client.finish
172
+ rescue
173
+ end
166
174
  end
167
175
 
168
176
  def synchronize_for_testing(flush_workers)
@@ -246,16 +254,17 @@ module LaunchDarkly
246
254
  end
247
255
 
248
256
  def handle_response(res)
249
- if res.status >= 400
250
- message = Util.http_error_message(res.status, "event delivery", "some events were dropped")
257
+ status = res.code.to_i
258
+ if status >= 400
259
+ message = Util.http_error_message(status, "event delivery", "some events were dropped")
251
260
  @config.logger.error { "[LDClient] #{message}" }
252
- if !Util.http_error_recoverable?(res.status)
261
+ if !Util.http_error_recoverable?(status)
253
262
  @disabled.value = true
254
263
  end
255
264
  else
256
- if !res.headers.nil? && res.headers.has_key?("Date")
265
+ if !res["date"].nil?
257
266
  begin
258
- res_time = (Time.httpdate(res.headers["Date"]).to_f * 1000).to_i
267
+ res_time = (Time.httpdate(res["date"]).to_f * 1000).to_i
259
268
  @last_known_past_time.value = res_time
260
269
  rescue ArgumentError
261
270
  end
@@ -316,22 +325,24 @@ module LaunchDarkly
316
325
  sleep(1)
317
326
  end
318
327
  begin
328
+ client.start if !client.started?
319
329
  config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
320
- res = client.post (config.events_uri + "/bulk") do |req|
321
- req.headers["Authorization"] = sdk_key
322
- req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
323
- req.headers["Content-Type"] = "application/json"
324
- req.headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
325
- req.body = body
326
- req.options.timeout = config.read_timeout
327
- req.options.open_timeout = config.connect_timeout
328
- end
330
+ uri = URI(config.events_uri + "/bulk")
331
+ req = Net::HTTP::Post.new(uri)
332
+ req.content_type = "application/json"
333
+ req.body = body
334
+ req["Authorization"] = sdk_key
335
+ req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
336
+ req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
337
+ req["Connection"] = "keep-alive"
338
+ res = client.request(req)
329
339
  rescue StandardError => exn
330
340
  config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
331
341
  next
332
342
  end
333
- if res.status < 200 || res.status >= 300
334
- if Util.http_error_recoverable?(res.status)
343
+ status = res.code.to_i
344
+ if status < 200 || status >= 300
345
+ if Util.http_error_recoverable?(status)
335
346
  next
336
347
  end
337
348
  end
@@ -26,7 +26,7 @@ module LaunchDarkly
26
26
 
27
27
  def stop
28
28
  if @stopped.make_true
29
- if @worker && @worker.alive?
29
+ if @worker && @worker.alive? && @worker != Thread.current
30
30
  @worker.run # causes the thread to wake up if it's currently in a sleep
31
31
  @worker.join
32
32
  end
@@ -63,8 +63,7 @@ module LaunchDarkly
63
63
  stop
64
64
  end
65
65
  rescue StandardError => exn
66
- @config.logger.error { "[LDClient] Exception while polling: #{exn.inspect}" }
67
- # TODO: log_exception(__method__.to_s, exn)
66
+ Util.log_exception(@config.logger, "Exception while polling", exn)
68
67
  end
69
68
  delta = @config.poll_interval - (Time.now - started_at)
70
69
  if delta > 0
@@ -1,12 +1,13 @@
1
+ require "concurrent/atomics"
1
2
  require "json"
2
- require "net/http/persistent"
3
- require "faraday/http_cache"
3
+ require "uri"
4
4
 
5
5
  module LaunchDarkly
6
6
  # @private
7
7
  class UnexpectedResponseError < StandardError
8
8
  def initialize(status)
9
9
  @status = status
10
+ super("HTTP error #{status}")
10
11
  end
11
12
 
12
13
  def status
@@ -16,14 +17,13 @@ module LaunchDarkly
16
17
 
17
18
  # @private
18
19
  class Requestor
20
+ CacheEntry = Struct.new(:etag, :body)
21
+
19
22
  def initialize(sdk_key, config)
20
23
  @sdk_key = sdk_key
21
24
  @config = config
22
- @client = Faraday.new do |builder|
23
- builder.use :http_cache, store: @config.cache_store, serializer: Marshal
24
-
25
- builder.adapter :net_http_persistent
26
- end
25
+ @client = Util.new_http_client(@config.base_uri, @config)
26
+ @cache = @config.cache_store
27
27
  end
28
28
 
29
29
  def request_flag(key)
@@ -38,27 +38,64 @@ module LaunchDarkly
38
38
  make_request("/sdk/latest-all")
39
39
  end
40
40
 
41
- def make_request(path)
42
- uri = @config.base_uri + path
43
- res = @client.get (uri) do |req|
44
- req.headers["Authorization"] = @sdk_key
45
- req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
46
- req.options.timeout = @config.read_timeout
47
- req.options.open_timeout = @config.connect_timeout
48
- if @config.proxy
49
- req.options.proxy = Faraday::ProxyOptions.from @config.proxy
50
- end
41
+ def stop
42
+ begin
43
+ @client.finish
44
+ rescue
51
45
  end
46
+ end
52
47
 
53
- @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{res.status}\n\theaders: #{res.headers}\n\tbody: #{res.body}" }
48
+ private
54
49
 
55
- if res.status < 200 || res.status >= 300
56
- raise UnexpectedResponseError.new(res.status)
50
+ def make_request(path)
51
+ @client.start if !@client.started?
52
+ uri = URI(@config.base_uri + path)
53
+ req = Net::HTTP::Get.new(uri)
54
+ req["Authorization"] = @sdk_key
55
+ req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
56
+ req["Connection"] = "keep-alive"
57
+ cached = @cache.read(uri)
58
+ if !cached.nil?
59
+ req["If-None-Match"] = cached.etag
57
60
  end
61
+ res = @client.request(req)
62
+ status = res.code.to_i
63
+ @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
58
64
 
59
- JSON.parse(res.body, symbolize_names: true)
65
+ if status == 304 && !cached.nil?
66
+ body = cached.body
67
+ else
68
+ @cache.delete(uri)
69
+ if status < 200 || status >= 300
70
+ raise UnexpectedResponseError.new(status)
71
+ end
72
+ body = fix_encoding(res.body, res["content-type"])
73
+ etag = res["etag"]
74
+ @cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
75
+ end
76
+ JSON.parse(body, symbolize_names: true)
77
+ end
78
+
79
+ def fix_encoding(body, content_type)
80
+ return body if content_type.nil?
81
+ media_type, charset = parse_content_type(content_type)
82
+ return body if charset.nil?
83
+ body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
60
84
  end
61
85
 
62
- private :make_request
86
+ def parse_content_type(value)
87
+ return [nil, nil] if value.nil? || value == ''
88
+ parts = value.split(/; */)
89
+ return [value, nil] if parts.count < 2
90
+ charset = nil
91
+ parts.each do |part|
92
+ fields = part.split('=')
93
+ if fields.count >= 2 && fields[0] == 'charset'
94
+ charset = fields[1]
95
+ break
96
+ end
97
+ end
98
+ return [parts[0], charset]
99
+ end
63
100
  end
64
101
  end
@@ -50,7 +50,6 @@ module LaunchDarkly
50
50
  }
51
51
  opts = {
52
52
  headers: headers,
53
- proxy: @config.proxy,
54
53
  read_timeout: READ_TIMEOUT_SECONDS,
55
54
  logger: @config.logger
56
55
  }
@@ -1,7 +1,17 @@
1
+ require "uri"
1
2
 
2
3
  module LaunchDarkly
3
4
  # @private
4
5
  module Util
6
+ def self.new_http_client(uri_s, config)
7
+ uri = URI(uri_s)
8
+ client = Net::HTTP.new(uri.hostname, uri.port)
9
+ client.use_ssl = true if uri.scheme == "https"
10
+ client.open_timeout = config.connect_timeout
11
+ client.read_timeout = config.read_timeout
12
+ client
13
+ end
14
+
5
15
  def self.log_exception(logger, message, exc)
6
16
  logger.error { "[LDClient] #{message}: #{exc.inspect}" }
7
17
  logger.debug { "[LDClient] Exception trace: #{exc.backtrace}" }
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.5.2"
2
+ VERSION = "5.5.3"
3
3
  end
@@ -1,5 +1,5 @@
1
+ require "http_util"
1
2
  require "spec_helper"
2
- require "faraday"
3
3
  require "time"
4
4
 
5
5
  describe LaunchDarkly::EventProcessor do
@@ -348,7 +348,7 @@ describe LaunchDarkly::EventProcessor do
348
348
  @ep.flush
349
349
  @ep.wait_until_inactive
350
350
 
351
- expect(hc.get_request.headers["Authorization"]).to eq "sdk_key"
351
+ expect(hc.get_request["authorization"]).to eq "sdk_key"
352
352
  end
353
353
 
354
354
  def verify_unrecoverable_http_error(status)
@@ -414,7 +414,7 @@ describe LaunchDarkly::EventProcessor do
414
414
  e = { kind: "identify", user: user }
415
415
  @ep.add_event(e)
416
416
 
417
- hc.set_exception(Faraday::Error::ConnectionFailed.new("fail"))
417
+ hc.set_exception(IOError.new("deliberate error"))
418
418
  @ep.flush
419
419
  @ep.wait_until_inactive
420
420
 
@@ -423,6 +423,46 @@ describe LaunchDarkly::EventProcessor do
423
423
  expect(hc.get_request).to be_nil # no 3rd request
424
424
  end
425
425
 
426
+ it "makes actual HTTP request with correct headers" do
427
+ e = { kind: "identify", key: user[:key], user: user }
428
+ with_server do |server|
429
+ server.setup_ok_response("/bulk", "")
430
+
431
+ @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
432
+ @ep.add_event(e)
433
+ @ep.flush
434
+
435
+ req = server.await_request
436
+ expect(req.header).to include({
437
+ "authorization" => [ "sdk_key" ],
438
+ "content-type" => [ "application/json" ],
439
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
440
+ "x-launchdarkly-event-schema" => [ "3" ]
441
+ })
442
+ end
443
+ end
444
+
445
+ it "can use a proxy server" do
446
+ e = { kind: "identify", key: user[:key], user: user }
447
+ with_server do |server|
448
+ server.setup_ok_response("/bulk", "")
449
+
450
+ with_server(StubProxyServer.new) do |proxy|
451
+ begin
452
+ ENV["http_proxy"] = proxy.base_uri.to_s
453
+ @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
454
+ @ep.add_event(e)
455
+ @ep.flush
456
+
457
+ req = server.await_request
458
+ expect(req["content-type"]).to eq("application/json")
459
+ ensure
460
+ ENV["http_proxy"] = nil
461
+ end
462
+ end
463
+ end
464
+ end
465
+
426
466
  def index_event(e, user)
427
467
  {
428
468
  kind: "index",
@@ -496,38 +536,42 @@ describe LaunchDarkly::EventProcessor do
496
536
  @status = 200
497
537
  end
498
538
 
499
- def post(uri)
500
- req = Faraday::Request.create("POST")
501
- req.headers = {}
502
- req.options = Faraday::RequestOptions.new
503
- yield req
539
+ def request(req)
504
540
  @requests.push(req)
505
541
  if @exception
506
542
  raise @exception
507
543
  else
508
- resp = Faraday::Response.new
509
544
  headers = {}
510
545
  if @server_time
511
546
  headers["Date"] = @server_time.httpdate
512
547
  end
513
- resp.finish({
514
- status: @status ? @status : 200,
515
- response_headers: headers
516
- })
517
- resp
548
+ FakeResponse.new(@status ? @status : 200, headers)
518
549
  end
519
550
  end
520
551
 
552
+ def start
553
+ end
554
+
555
+ def started?
556
+ false
557
+ end
558
+
559
+ def finish
560
+ end
561
+
521
562
  def get_request
522
563
  @requests.shift
523
564
  end
524
565
  end
525
566
 
526
567
  class FakeResponse
527
- def initialize(status)
528
- @status = status
529
- end
568
+ include Net::HTTPHeader
530
569
 
531
- attr_reader :status
570
+ attr_reader :code
571
+
572
+ def initialize(status, headers)
573
+ @code = status.to_s
574
+ initialize_http_header(headers)
575
+ end
532
576
  end
533
577
  end
@@ -74,7 +74,7 @@ flagValues:
74
74
  segments:
75
75
  seg1:
76
76
  key: seg1
77
- include: ["user1"]
77
+ include: ["user1"]
78
78
  EOF
79
79
  }
80
80
 
@@ -87,7 +87,7 @@ EOF
87
87
  end
88
88
 
89
89
  after do
90
- FileUtils.remove_dir(@tmp_dir)
90
+ FileUtils.rm_rf(@tmp_dir)
91
91
  end
92
92
 
93
93
  def make_temp_file(content)
@@ -198,10 +198,10 @@ EOF
198
198
  event = ds.start
199
199
  expect(event.set?).to eq(true)
200
200
  expect(@store.all(LaunchDarkly::SEGMENTS).keys).to eq([])
201
-
201
+
202
202
  sleep(1)
203
203
  IO.write(file, all_properties_json)
204
-
204
+
205
205
  max_time = 10
206
206
  ok = wait_for_condition(10) { @store.all(LaunchDarkly::SEGMENTS).keys == all_segment_keys }
207
207
  expect(ok).to eq(true), "Waited #{max_time}s after modifying file and it did not reload"
@@ -243,7 +243,7 @@ EOF
243
243
  client.close
244
244
  end
245
245
  end
246
-
246
+
247
247
  def wait_for_condition(max_time)
248
248
  deadline = Time.now + max_time
249
249
  while Time.now < deadline
@@ -23,6 +23,7 @@ class StubHTTPServer
23
23
  retry
24
24
  end
25
25
  @requests = []
26
+ @requests_queue = Queue.new
26
27
  end
27
28
 
28
29
  def self.next_port
@@ -62,6 +63,11 @@ class StubHTTPServer
62
63
 
63
64
  def record_request(req, res)
64
65
  @requests.push(req)
66
+ @requests_queue << req
67
+ end
68
+
69
+ def await_request
70
+ @requests_queue.pop
65
71
  end
66
72
  end
67
73
 
@@ -1,57 +1,212 @@
1
1
  require "http_util"
2
2
  require "spec_helper"
3
3
 
4
+ $sdk_key = "secret"
5
+
4
6
  describe LaunchDarkly::Requestor do
5
- describe ".request_all_flags" do
6
- describe "with a proxy" do
7
- it "converts the proxy option" do
8
- content = '{"flags": {"flagkey": {"key": "flagkey"}}}'
9
- with_server do |server|
10
- server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
11
- with_server(StubProxyServer.new) do |proxy|
12
- config = LaunchDarkly::Config.new(base_uri: server.base_uri.to_s, proxy: proxy.base_uri.to_s)
13
- r = LaunchDarkly::Requestor.new("sdk-key", config)
14
- result = r.request_all_data
15
- expect(result).to eq(JSON.parse(content, symbolize_names: true))
16
- end
17
- end
18
- end
19
- end
20
- describe "without a proxy" do
21
- it "sends headers" do
22
- content = '{"flags": {}}'
23
- sdk_key = 'sdk-key'
24
- with_server do |server|
25
- server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
26
- r = LaunchDarkly::Requestor.new(sdk_key, LaunchDarkly::Config.new({ base_uri: server.base_uri.to_s }))
27
- r.request_all_data
28
- expect(server.requests.length).to eq 1
29
- req = server.requests[0]
30
- expect(req.header['authorization']).to eq [sdk_key]
31
- expect(req.header['user-agent']).to eq ["RubyClient/" + LaunchDarkly::VERSION]
32
- end
33
- end
34
-
35
- it "receives data" do
36
- content = '{"flags": {"flagkey": {"key": "flagkey"}}}'
37
- with_server do |server|
38
- server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
39
- r = LaunchDarkly::Requestor.new("sdk-key", LaunchDarkly::Config.new({ base_uri: server.base_uri.to_s }))
40
- result = r.request_all_data
41
- expect(result).to eq(JSON.parse(content, symbolize_names: true))
42
- end
43
- end
44
-
45
- it "handles Unicode content" do
46
- content = '{"flags": {"flagkey": {"key": "flagkey", "variations": ["blue", "grėeń"]}}}'
47
- with_server do |server|
48
- server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
49
- # Note that the ETag header here is important because without it, the HTTP cache will not be used,
50
- # and the cache is what required a fix to handle Unicode properly. See:
51
- # https://github.com/launchdarkly/ruby-client/issues/90
52
- r = LaunchDarkly::Requestor.new("sdk-key", LaunchDarkly::Config.new({ base_uri: server.base_uri.to_s }))
53
- result = r.request_all_data
54
- expect(result).to eq(JSON.parse(content, symbolize_names: true))
7
+ def with_requestor(base_uri)
8
+ r = LaunchDarkly::Requestor.new($sdk_key, LaunchDarkly::Config.new(base_uri: base_uri))
9
+ yield r
10
+ r.stop
11
+ end
12
+
13
+ describe "request_all_flags" do
14
+ it "uses expected URI and headers" do
15
+ with_server do |server|
16
+ with_requestor(server.base_uri.to_s) do |requestor|
17
+ server.setup_ok_response("/", "{}")
18
+ requestor.request_all_data()
19
+ expect(server.requests.count).to eq 1
20
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-all"
21
+ expect(server.requests[0].header).to include({
22
+ "authorization" => [ $sdk_key ],
23
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
24
+ })
25
+ end
26
+ end
27
+ end
28
+
29
+ it "parses response" do
30
+ expected_data = { flags: { x: { key: "x" } } }
31
+ with_server do |server|
32
+ with_requestor(server.base_uri.to_s) do |requestor|
33
+ server.setup_ok_response("/", expected_data.to_json)
34
+ data = requestor.request_all_data()
35
+ expect(data).to eq expected_data
36
+ end
37
+ end
38
+ end
39
+
40
+ it "sends etag from previous response" do
41
+ etag = "xyz"
42
+ with_server do |server|
43
+ with_requestor(server.base_uri.to_s) do |requestor|
44
+ server.setup_response("/") do |req, res|
45
+ res.status = 200
46
+ res.body = "{}"
47
+ res["ETag"] = etag
48
+ end
49
+ requestor.request_all_data()
50
+ expect(server.requests.count).to eq 1
51
+
52
+ requestor.request_all_data()
53
+ expect(server.requests.count).to eq 2
54
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag ] })
55
+ end
56
+ end
57
+ end
58
+
59
+ it "can reuse cached data" do
60
+ etag = "xyz"
61
+ expected_data = { flags: { x: { key: "x" } } }
62
+ with_server do |server|
63
+ with_requestor(server.base_uri.to_s) do |requestor|
64
+ server.setup_response("/") do |req, res|
65
+ res.status = 200
66
+ res.body = expected_data.to_json
67
+ res["ETag"] = etag
68
+ end
69
+ requestor.request_all_data()
70
+ expect(server.requests.count).to eq 1
71
+
72
+ server.setup_response("/") do |req, res|
73
+ res.status = 304
74
+ end
75
+ data = requestor.request_all_data()
76
+ expect(server.requests.count).to eq 2
77
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag ] })
78
+ expect(data).to eq expected_data
79
+ end
80
+ end
81
+ end
82
+
83
+ it "replaces cached data with new data" do
84
+ etag1 = "abc"
85
+ etag2 = "xyz"
86
+ expected_data1 = { flags: { x: { key: "x" } } }
87
+ expected_data2 = { flags: { y: { key: "y" } } }
88
+ with_server do |server|
89
+ with_requestor(server.base_uri.to_s) do |requestor|
90
+ server.setup_response("/") do |req, res|
91
+ res.status = 200
92
+ res.body = expected_data1.to_json
93
+ res["ETag"] = etag1
94
+ end
95
+ data = requestor.request_all_data()
96
+ expect(data).to eq expected_data1
97
+ expect(server.requests.count).to eq 1
98
+
99
+ server.setup_response("/") do |req, res|
100
+ res.status = 304
101
+ end
102
+ data = requestor.request_all_data()
103
+ expect(data).to eq expected_data1
104
+ expect(server.requests.count).to eq 2
105
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag1 ] })
106
+
107
+ server.setup_response("/") do |req, res|
108
+ res.status = 200
109
+ res.body = expected_data2.to_json
110
+ res["ETag"] = etag2
111
+ end
112
+ data = requestor.request_all_data()
113
+ expect(data).to eq expected_data2
114
+ expect(server.requests.count).to eq 3
115
+ expect(server.requests[2].header).to include({ "if-none-match" => [ etag1 ] })
116
+
117
+ server.setup_response("/") do |req, res|
118
+ res.status = 304
119
+ end
120
+ data = requestor.request_all_data()
121
+ expect(data).to eq expected_data2
122
+ expect(server.requests.count).to eq 4
123
+ expect(server.requests[3].header).to include({ "if-none-match" => [ etag2 ] })
124
+ end
125
+ end
126
+ end
127
+
128
+ it "uses UTF-8 encoding by default" do
129
+ content = '{"flags": {"flagkey": {"key": "flagkey", "variations": ["blue", "grėeń"]}}}'
130
+ with_server do |server|
131
+ server.setup_ok_response("/sdk/latest-all", content, "application/json")
132
+ with_requestor(server.base_uri.to_s) do |requestor|
133
+ data = requestor.request_all_data
134
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
135
+ end
136
+ end
137
+ end
138
+
139
+ it "detects other encodings from Content-Type" do
140
+ content = '{"flags": {"flagkey": {"key": "flagkey", "variations": ["proszę", "dziękuję"]}}}'
141
+ with_server do |server|
142
+ server.setup_ok_response("/sdk/latest-all", content.encode(Encoding::ISO_8859_2),
143
+ "text/plain; charset=ISO-8859-2")
144
+ with_requestor(server.base_uri.to_s) do |requestor|
145
+ data = requestor.request_all_data
146
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
147
+ end
148
+ end
149
+ end
150
+
151
+ it "throws exception for error status" do
152
+ with_server do |server|
153
+ with_requestor(server.base_uri.to_s) do |requestor|
154
+ server.setup_response("/") do |req, res|
155
+ res.status = 400
156
+ end
157
+ expect { requestor.request_all_data() }.to raise_error(LaunchDarkly::UnexpectedResponseError)
158
+ end
159
+ end
160
+ end
161
+
162
+ it "can use a proxy server" do
163
+ content = '{"flags": {"flagkey": {"key": "flagkey"}}}'
164
+ with_server do |server|
165
+ server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
166
+ with_server(StubProxyServer.new) do |proxy|
167
+ begin
168
+ ENV["http_proxy"] = proxy.base_uri.to_s
169
+ with_requestor(server.base_uri.to_s) do |requestor|
170
+ data = requestor.request_all_data
171
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
172
+ end
173
+ ensure
174
+ ENV["http_proxy"] = nil
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "request_flag" do
182
+ it "uses expected URI and headers" do
183
+ with_server do |server|
184
+ with_requestor(server.base_uri.to_s) do |requestor|
185
+ server.setup_ok_response("/", "{}")
186
+ requestor.request_flag("key")
187
+ expect(server.requests.count).to eq 1
188
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-flags/key"
189
+ expect(server.requests[0].header).to include({
190
+ "authorization" => [ $sdk_key ],
191
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
192
+ })
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "request_segment" do
199
+ it "uses expected URI and headers" do
200
+ with_server do |server|
201
+ with_requestor(server.base_uri.to_s) do |requestor|
202
+ server.setup_ok_response("/", "{}")
203
+ requestor.request_segment("key")
204
+ expect(server.requests.count).to eq 1
205
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-segments/key"
206
+ expect(server.requests[0].header).to include({
207
+ "authorization" => [ $sdk_key ],
208
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
209
+ })
55
210
  end
56
211
  end
57
212
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ldclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.2
4
+ version: 5.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-18 00:00:00.000000000 Z
11
+ date: 2019-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -184,46 +184,6 @@ dependencies:
184
184
  - - "<"
185
185
  - !ruby/object:Gem::Version
186
186
  version: '3'
187
- - !ruby/object:Gem::Dependency
188
- name: faraday
189
- requirement: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - ">="
192
- - !ruby/object:Gem::Version
193
- version: '0.9'
194
- - - "<"
195
- - !ruby/object:Gem::Version
196
- version: '2'
197
- type: :runtime
198
- prerelease: false
199
- version_requirements: !ruby/object:Gem::Requirement
200
- requirements:
201
- - - ">="
202
- - !ruby/object:Gem::Version
203
- version: '0.9'
204
- - - "<"
205
- - !ruby/object:Gem::Version
206
- version: '2'
207
- - !ruby/object:Gem::Dependency
208
- name: faraday-http-cache
209
- requirement: !ruby/object:Gem::Requirement
210
- requirements:
211
- - - ">="
212
- - !ruby/object:Gem::Version
213
- version: 1.3.0
214
- - - "<"
215
- - !ruby/object:Gem::Version
216
- version: '3'
217
- type: :runtime
218
- prerelease: false
219
- version_requirements: !ruby/object:Gem::Requirement
220
- requirements:
221
- - - ">="
222
- - !ruby/object:Gem::Version
223
- version: 1.3.0
224
- - - "<"
225
- - !ruby/object:Gem::Version
226
- version: '3'
227
187
  - !ruby/object:Gem::Dependency
228
188
  name: semantic
229
189
  requirement: !ruby/object:Gem::Requirement
@@ -238,26 +198,6 @@ dependencies:
238
198
  - - "~>"
239
199
  - !ruby/object:Gem::Version
240
200
  version: '1.6'
241
- - !ruby/object:Gem::Dependency
242
- name: net-http-persistent
243
- requirement: !ruby/object:Gem::Requirement
244
- requirements:
245
- - - ">="
246
- - !ruby/object:Gem::Version
247
- version: '2.9'
248
- - - "<"
249
- - !ruby/object:Gem::Version
250
- version: '4.0'
251
- type: :runtime
252
- prerelease: false
253
- version_requirements: !ruby/object:Gem::Requirement
254
- requirements:
255
- - - ">="
256
- - !ruby/object:Gem::Version
257
- version: '2.9'
258
- - - "<"
259
- - !ruby/object:Gem::Version
260
- version: '4.0'
261
201
  - !ruby/object:Gem::Dependency
262
202
  name: concurrent-ruby
263
203
  requirement: !ruby/object:Gem::Requirement
@@ -309,6 +249,7 @@ files:
309
249
  - LICENSE.txt
310
250
  - README.md
311
251
  - Rakefile
252
+ - azure-pipelines.yml
312
253
  - ext/mkrf_conf.rb
313
254
  - ldclient-rb.gemspec
314
255
  - lib/ldclient-rb.rb