ldclient-rb 5.5.2 → 5.5.3

Sign up to get free protection for your applications and to get access to all the features.
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