ldclient-rb 4.0.0 → 5.0.0

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: 65f975d234beca67eed5238b9e693fdfb39de00e
4
- data.tar.gz: 93e6c8e3de9f9394f1cf9acb64218fda1b873ff6
3
+ metadata.gz: 743c38bedf7c153667c5a38fcc2deaffd557f09d
4
+ data.tar.gz: c352ed4f8c412e2c19edcbe94d5813a2478ddade
5
5
  SHA512:
6
- metadata.gz: 1aacb9669181a5cee75fcfdad3170a195bd43bb572203251daa0fd892e94ecf95bd8627ee124fa961884a44d38209271e9f9fbb094a7773b123328d33011e6ab
7
- data.tar.gz: 12b7baa6f4176b357b11307faf9588a918e34059d1a5bc2fc97e5c6076f25ad1b5d5f13b328b4295d4c1bd4a90dfb3fa02daf75549886948e5311c2b91753bed
6
+ metadata.gz: 6add89667ba084d57d7e49b52ced7ff279aa491aabc2285354353ce6b6f8393f14c04bef18062b1762ab6cf0bc97b27060009e589f78523fe8acaba9c9ff4a3d
7
+ data.tar.gz: 0a46454ca002c7fc35d15fc2343097727552cdc81ef517d96032befe77a451ddbd9e3f99b048cec5e132480c4ebb124685a8cdd282c6982f1c40844ceef7313b
data/.circleci/config.yml CHANGED
@@ -8,7 +8,8 @@ workflows:
8
8
  - test-2.2
9
9
  - test-2.3
10
10
  - test-2.4
11
- - test-jruby-9.1
11
+ - test-2.5
12
+ - test-jruby-9.2
12
13
 
13
14
  ruby-docker-template: &ruby-docker-template
14
15
  steps:
@@ -17,6 +18,7 @@ ruby-docker-template: &ruby-docker-template
17
18
  if [[ $CIRCLE_JOB == test-jruby* ]]; then
18
19
  gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
19
20
  fi
21
+ - run: ruby -v
20
22
  - run: gem install bundler
21
23
  - run: bundle install
22
24
  - run: mkdir ./rspec
@@ -40,9 +42,14 @@ jobs:
40
42
  test-2.4:
41
43
  <<: *ruby-docker-template
42
44
  docker:
43
- - image: circleci/ruby:2.4.3-jessie
45
+ - image: circleci/ruby:2.4.4-stretch
44
46
  - image: redis
45
- test-jruby-9.1:
47
+ test-2.5:
48
+ <<: *ruby-docker-template
49
+ docker:
50
+ - image: circleci/ruby:2.5.1-stretch
51
+ - image: redis
52
+ test-jruby-9.2:
46
53
  <<: *ruby-docker-template
47
54
  docker:
48
55
  - image: circleci/jruby:9-jdk
@@ -54,7 +61,7 @@ jobs:
54
61
  machine:
55
62
  image: circleci/classic:latest
56
63
  environment:
57
- - RUBIES: "ruby-2.1.9 ruby-2.0.0 ruby-1.9.3 jruby-9.0.5.0"
64
+ - RUBIES: "jruby-9.1.17.0"
58
65
  steps:
59
66
  - run: sudo apt-get -q update
60
67
  - run: sudo apt-get -qy install redis-server
data/CHANGELOG.md CHANGED
@@ -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.0.0] - 2018-06-26
6
+ ### Changed:
7
+ - The client no longer uses Celluloid for streaming I/O. Instead, it uses [socketry](https://github.com/socketry/socketry).
8
+ - The client now treats most HTTP 4xx errors as unrecoverable: that is, after receiving such an error, it will not make any more HTTP requests for the lifetime of the client instance, in effect taking the client offline. This is because such errors indicate either a configuration problem (invalid SDK key) or a bug, which is not likely to resolve without a restart or an upgrade. This does not apply if the error is 400, 408, 429, or any 5xx error.
9
+ - During initialization, if the client receives any of the unrecoverable errors described above, the client constructor will return immediately; previously it would continue waiting until a timeout. The `initialized?` method will return false in this case.
10
+
11
+ ### Removed:
12
+ - The SDK no longer supports Ruby versions below 2.2.6, or JRuby below 9.1.16.
13
+
5
14
  ## [4.0.0] - 2018-05-10
6
15
 
7
16
  ### Changed:
data/README.md CHANGED
@@ -7,15 +7,19 @@ LaunchDarkly SDK for Ruby
7
7
  [![Test Coverage](https://codeclimate.com/github/launchdarkly/ruby-client/badges/coverage.svg)](https://codeclimate.com/github/launchdarkly/ruby-client/coverage)
8
8
  [![security](https://hakiri.io/github/launchdarkly/ruby-client/master.svg)](https://hakiri.io/github/launchdarkly/ruby-client/master)
9
9
 
10
+ Supported Ruby versions
11
+ -----------------------
12
+
13
+ This version of the LaunchDarkly SDK has a minimum Ruby version of 2.2.6, or 9.1.6 for JRuby.
14
+
10
15
  Quick setup
11
16
  -----------
12
17
 
13
18
  0. Install the Ruby SDK with `gem`
14
19
 
15
20
  ```shell
16
- gem install ldclient-rb --prerelease
21
+ gem install ldclient-rb
17
22
  ```
18
- Note: The `--prerelease` flag is there to satisfy the dependency of celluloid 0.18pre which we have tested extensively and have found stable in our use case. Unfortunately, the upstream provider has not promoted this version to stable yet. See [here](https://github.com/celluloid/celluloid/issues/762) This is not required for use in a Gemfile.
19
23
 
20
24
  1. Require the LaunchDarkly client:
21
25
 
@@ -79,7 +83,7 @@ Note that this gem will automatically switch to using the Rails logger it is det
79
83
 
80
84
  HTTPS proxy
81
85
  ------------
82
- The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides built-in support for the use of an HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
86
+ The Ruby SDK uses Faraday and Socketry to handle its network traffic. Both of these provide built-in support for the use of an HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
83
87
 
84
88
  How to set the HTTPS_PROXY environment variable on Mac/Linux systems:
85
89
  ```
data/ldclient-rb.gemspec CHANGED
@@ -26,36 +26,18 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
27
27
  spec.add_development_dependency "redis", "~> 3.3.5"
28
28
  spec.add_development_dependency "connection_pool", ">= 2.1.2"
29
- if RUBY_VERSION >= "2.0.0"
30
- spec.add_development_dependency "rake", "~> 10.0"
31
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
32
- else
33
- spec.add_development_dependency "rake", "12.1.0"
34
- # higher versions of rake fail to install in JRuby 1.7
35
- end
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
36
31
  spec.add_development_dependency "timecop", "~> 0.9.1"
37
32
 
38
33
  spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
39
- if RUBY_VERSION >= "2.1.0"
40
- spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
41
- spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
42
- else
43
- spec.add_runtime_dependency "faraday", [">= 0.9", "< 0.14.0"]
44
- spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 2"]
45
- end
34
+ spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
35
+ spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
46
36
  spec.add_runtime_dependency "semantic", "~> 1.6.0"
47
37
  spec.add_runtime_dependency "thread_safe", "~> 0.3"
48
38
  spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
49
39
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.4"
50
40
  spec.add_runtime_dependency "hashdiff", "~> 0.2"
51
- spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.11.0"
52
- spec.add_runtime_dependency "celluloid", "~> 0.18.0.pre" # transitive dep; specified here for more control
53
-
54
- if RUBY_VERSION >= "2.2.2"
55
- spec.add_runtime_dependency "nio4r", "< 3" # for maximum ruby version compatibility.
56
- else
57
- spec.add_runtime_dependency "nio4r", "~> 1.1" # for maximum ruby version compatibility.
58
- end
59
-
60
- spec.add_runtime_dependency "waitutil", "0.2"
41
+ spec.add_runtime_dependency "http_tools", '~> 0.4.5'
42
+ spec.add_runtime_dependency "socketry", "~> 0.5.1"
61
43
  end
data/lib/ldclient-rb.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "ldclient-rb/version"
2
+ require "ldclient-rb/util"
2
3
  require "ldclient-rb/evaluation"
3
4
  require "ldclient-rb/ldclient"
4
5
  require "ldclient-rb/cache_store"
@@ -222,17 +222,24 @@ module LaunchDarkly
222
222
  if !payload.events.empty? || !payload.summary.counters.empty?
223
223
  # If all available worker threads are busy, success will be false and no job will be queued.
224
224
  success = flush_workers.post do
225
- resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
226
- handle_response(resp) if !resp.nil?
225
+ begin
226
+ resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
227
+ handle_response(resp) if !resp.nil?
228
+ rescue => e
229
+ @config.logger.warn { "[LDClient] Unexpected error in event processor: #{e.inspect}. \nTrace: #{e.backtrace}" }
230
+ end
227
231
  end
228
232
  buffer.clear if success # Reset our internal state, these events now belong to the flush worker
229
233
  end
230
234
  end
231
235
 
232
236
  def handle_response(res)
233
- if res.status == 401
234
- @config.logger.error { "[LDClient] Received 401 error, no further events will be posted since SDK key is invalid" }
235
- @disabled.value = true
237
+ if res.status >= 400
238
+ message = Util.http_error_message(res.status, "event delivery", "some events were dropped")
239
+ @config.logger.error { "[LDClient] #{message}" }
240
+ if !Util.http_error_recoverable?(res.status)
241
+ @disabled.value = true
242
+ end
236
243
  else
237
244
  if !res.headers.nil? && res.headers.has_key?("Date")
238
245
  begin
@@ -309,8 +316,7 @@ module LaunchDarkly
309
316
  next
310
317
  end
311
318
  if res.status < 200 || res.status >= 300
312
- config.logger.error { "[LDClient] Unexpected status code while processing events: #{res.status}" }
313
- if res.status >= 500
319
+ if Util.http_error_recoverable?(res.status)
314
320
  next
315
321
  end
316
322
  end
@@ -1,7 +1,7 @@
1
+ require "concurrent/atomics"
1
2
  require "digest/sha1"
2
3
  require "logger"
3
4
  require "benchmark"
4
- require "waitutil"
5
5
  require "json"
6
6
  require "openssl"
7
7
 
@@ -41,7 +41,9 @@ module LaunchDarkly
41
41
 
42
42
  requestor = Requestor.new(sdk_key, config)
43
43
 
44
- if !@config.offline?
44
+ if @config.offline?
45
+ @update_processor = NullUpdateProcessor.new
46
+ else
45
47
  if @config.update_processor.nil?
46
48
  if @config.stream?
47
49
  @update_processor = StreamProcessor.new(sdk_key, config, requestor)
@@ -53,16 +55,15 @@ module LaunchDarkly
53
55
  else
54
56
  @update_processor = @config.update_processor
55
57
  end
56
- @update_processor.start
57
58
  end
58
59
 
59
- if !@config.offline? && wait_for_sec > 0
60
- begin
61
- WaitUtil.wait_for_condition("LaunchDarkly client initialization", timeout_sec: wait_for_sec, delay_sec: 0.1) do
62
- initialized?
63
- end
64
- rescue WaitUtil::TimeoutError
60
+ ready = @update_processor.start
61
+ if wait_for_sec > 0
62
+ ok = ready.wait(wait_for_sec)
63
+ if !ok
65
64
  @config.logger.error { "[LDClient] Timeout encountered waiting for LaunchDarkly client initialization" }
65
+ elsif !@update_processor.initialized?
66
+ @config.logger.error { "[LDClient] LaunchDarkly client initialization failed" }
66
67
  end
67
68
  end
68
69
  end
@@ -220,9 +221,7 @@ module LaunchDarkly
220
221
  # @return [void]
221
222
  def close
222
223
  @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
223
- if not @config.offline?
224
- @update_processor.stop
225
- end
224
+ @update_processor.stop
226
225
  @event_processor.stop
227
226
  @store.stop
228
227
  end
@@ -255,4 +254,22 @@ module LaunchDarkly
255
254
 
256
255
  private :evaluate, :log_exception, :sanitize_user, :make_feature_event
257
256
  end
257
+
258
+ #
259
+ # Used internally when the client is offline.
260
+ #
261
+ class NullUpdateProcessor
262
+ def start
263
+ e = Concurrent::Event.new
264
+ e.set
265
+ e
266
+ end
267
+
268
+ def initialized?
269
+ true
270
+ end
271
+
272
+ def stop
273
+ end
274
+ end
258
275
  end
@@ -9,6 +9,7 @@ module LaunchDarkly
9
9
  @initialized = Concurrent::AtomicBoolean.new(false)
10
10
  @started = Concurrent::AtomicBoolean.new(false)
11
11
  @stopped = Concurrent::AtomicBoolean.new(false)
12
+ @ready = Concurrent::Event.new
12
13
  end
13
14
 
14
15
  def initialized?
@@ -16,9 +17,10 @@ module LaunchDarkly
16
17
  end
17
18
 
18
19
  def start
19
- return unless @started.make_true
20
+ return @ready unless @started.make_true
20
21
  @config.logger.info { "[LDClient] Initializing polling connection" }
21
22
  create_worker
23
+ @ready
22
24
  end
23
25
 
24
26
  def stop
@@ -39,6 +41,7 @@ module LaunchDarkly
39
41
  })
40
42
  if @initialized.make_true
41
43
  @config.logger.info { "[LDClient] Polling connection initialized" }
44
+ @ready.set
42
45
  end
43
46
  end
44
47
  end
@@ -47,20 +50,24 @@ module LaunchDarkly
47
50
  @worker = Thread.new do
48
51
  @config.logger.debug { "[LDClient] Starting polling worker" }
49
52
  while !@stopped.value do
53
+ started_at = Time.now
50
54
  begin
51
- started_at = Time.now
52
55
  poll
53
- delta = @config.poll_interval - (Time.now - started_at)
54
- if delta > 0
55
- sleep(delta)
56
+ rescue UnexpectedResponseError => e
57
+ message = Util.http_error_message(e.status, "polling request", "will retry")
58
+ @config.logger.error { "[LDClient] #{message}" };
59
+ if !Util.http_error_recoverable?(e.status)
60
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
61
+ stop
56
62
  end
57
- rescue InvalidSDKKeyError
58
- @config.logger.error { "[LDClient] Received 401 error, no further polling requests will be made since SDK key is invalid" };
59
- stop
60
63
  rescue StandardError => exn
61
64
  @config.logger.error { "[LDClient] Exception while polling: #{exn.inspect}" }
62
65
  # TODO: log_exception(__method__.to_s, exn)
63
66
  end
67
+ delta = @config.poll_interval - (Time.now - started_at)
68
+ if delta > 0
69
+ sleep(delta)
70
+ end
64
71
  end
65
72
  end
66
73
  end
@@ -4,7 +4,14 @@ require "faraday/http_cache"
4
4
 
5
5
  module LaunchDarkly
6
6
 
7
- class InvalidSDKKeyError < StandardError
7
+ class UnexpectedResponseError < StandardError
8
+ def initialize(status)
9
+ @status = status
10
+ end
11
+
12
+ def status
13
+ @status
14
+ end
8
15
  end
9
16
 
10
17
  class Requestor
@@ -13,7 +20,7 @@ module LaunchDarkly
13
20
  @config = config
14
21
  @client = Faraday.new do |builder|
15
22
  builder.use :http_cache, store: @config.cache_store
16
-
23
+
17
24
  builder.adapter :net_http_persistent
18
25
  end
19
26
  end
@@ -44,19 +51,8 @@ module LaunchDarkly
44
51
 
45
52
  @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{res.status}\n\theaders: #{res.headers}\n\tbody: #{res.body}" }
46
53
 
47
- if res.status == 401
48
- @config.logger.error { "[LDClient] Invalid SDK key" }
49
- raise InvalidSDKKeyError
50
- end
51
-
52
- if res.status == 404
53
- @config.logger.error { "[LDClient] Resource not found" }
54
- return nil
55
- end
56
-
57
54
  if res.status < 200 || res.status >= 300
58
- @config.logger.error { "[LDClient] Unexpected status code #{res.status}" }
59
- return nil
55
+ raise UnexpectedResponseError.new(res.status)
60
56
  end
61
57
 
62
58
  JSON.parse(res.body, symbolize_names: true)
@@ -1,6 +1,6 @@
1
1
  require "concurrent/atomics"
2
2
  require "json"
3
- require "celluloid/eventsource"
3
+ require "sse_client"
4
4
 
5
5
  module LaunchDarkly
6
6
  PUT = :put
@@ -24,6 +24,7 @@ module LaunchDarkly
24
24
  @initialized = Concurrent::AtomicBoolean.new(false)
25
25
  @started = Concurrent::AtomicBoolean.new(false)
26
26
  @stopped = Concurrent::AtomicBoolean.new(false)
27
+ @ready = Concurrent::Event.new
27
28
  end
28
29
 
29
30
  def initialized?
@@ -31,37 +32,34 @@ module LaunchDarkly
31
32
  end
32
33
 
33
34
  def start
34
- return unless @started.make_true
35
+ return @ready unless @started.make_true
35
36
 
36
37
  @config.logger.info { "[LDClient] Initializing stream connection" }
37
38
 
38
- headers =
39
- {
39
+ headers = {
40
40
  'Authorization' => @sdk_key,
41
41
  'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
42
42
  }
43
- opts = {:headers => headers, :with_credentials => true, :proxy => @config.proxy, :read_timeout => READ_TIMEOUT_SECONDS}
44
- @es = Celluloid::EventSource.new(@config.stream_uri + "/all", opts) do |conn|
45
- conn.on(PUT) { |message| process_message(message, PUT) }
46
- conn.on(PATCH) { |message| process_message(message, PATCH) }
47
- conn.on(DELETE) { |message| process_message(message, DELETE) }
48
- conn.on(INDIRECT_PUT) { |message| process_message(message, INDIRECT_PUT) }
49
- conn.on(INDIRECT_PATCH) { |message| process_message(message, INDIRECT_PATCH) }
43
+ opts = {
44
+ headers: headers,
45
+ proxy: @config.proxy,
46
+ read_timeout: READ_TIMEOUT_SECONDS,
47
+ logger: @config.logger
48
+ }
49
+ @es = SSE::SSEClient.new(@config.stream_uri + "/all", opts) do |conn|
50
+ conn.on_event { |event| process_message(event, event.type) }
50
51
  conn.on_error { |err|
51
- @config.logger.error { "[LDClient] Unexpected status code #{err[:status_code]} from streaming connection" }
52
- if err[:status_code] == 401
53
- @config.logger.error { "[LDClient] Received 401 error, no further streaming connection will be made since SDK key is invalid" }
52
+ status = err[:status_code]
53
+ message = Util.http_error_message(status, "streaming connection", "will retry")
54
+ @config.logger.error { "[LDClient] #{message}" }
55
+ if !Util.http_error_recoverable?(status)
56
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
54
57
  stop
55
58
  end
56
59
  }
57
60
  end
58
- end
59
-
60
- def stop
61
- if @stopped.make_true
62
- @es.close
63
- @config.logger.info { "[LDClient] Stream connection stopped" }
64
- end
61
+
62
+ @ready
65
63
  end
66
64
 
67
65
  def stop
@@ -83,21 +81,22 @@ module LaunchDarkly
83
81
  })
84
82
  @initialized.make_true
85
83
  @config.logger.info { "[LDClient] Stream initialized" }
84
+ @ready.set
86
85
  elsif method == PATCH
87
- message = JSON.parse(message.data, symbolize_names: true)
86
+ data = JSON.parse(message.data, symbolize_names: true)
88
87
  for kind in [FEATURES, SEGMENTS]
89
- key = key_for_path(kind, message[:path])
88
+ key = key_for_path(kind, data[:path])
90
89
  if key
91
- @feature_store.upsert(kind, message[:data])
90
+ @feature_store.upsert(kind, data[:data])
92
91
  break
93
92
  end
94
93
  end
95
94
  elsif method == DELETE
96
- message = JSON.parse(message.data, symbolize_names: true)
95
+ data = JSON.parse(message.data, symbolize_names: true)
97
96
  for kind in [FEATURES, SEGMENTS]
98
- key = key_for_path(kind, message[:path])
97
+ key = key_for_path(kind, data[:path])
99
98
  if key
100
- @feature_store.delete(kind, key, message[:version])
99
+ @feature_store.delete(kind, key, data[:version])
101
100
  break
102
101
  end
103
102
  end