featurehub-sdk 1.1.0 → 1.2.1

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
  SHA256:
3
- metadata.gz: c4621f1ab8e515ab8eec3b701bf3928d1eabfafbe21a955175729dfeb39ed31b
4
- data.tar.gz: 3e57a7dc1e0e525e6da0dca8848d42fd09eea437a378162160b5e529422fa12c
3
+ metadata.gz: 64f6c49d496d101322385a5ecd391efc0306f8d4b5e98bade99018492dda76c8
4
+ data.tar.gz: 11e43513703e3a5a343e84b57a7fa95237b3cfeb87111d175f2c82de9b3dad27
5
5
  SHA512:
6
- metadata.gz: 95899cd71c1c70756cddc97821c30173ef5381c0ba384d58e9be0b6c57664b59be85e4f748d55723677b84dfdda276df48526a8bb603165da911af1fc0344305
7
- data.tar.gz: 8840b4aa7359fa5bbbc3247bac92df4fc96915c788737d08e3d4a0ccd92542536e05b527411875f17b4a5b94b7614f033303eaba93d349a88be200e010f8ccef
6
+ metadata.gz: 7611bba9948b3d61c54d4e94470a06674795f0f2616b741a2b564cfd7c360378e8e58c4f12d70620c37bf4dcfa13c495b9e71428edeae493f470009c9464fce6
7
+ data.tar.gz: 5e3fe11fc58a9240fb2196d6af37aa8dd225f949a3a6c7adee2175c9e460116a51d34889f5ec283a1c57f0df2bb9c8170492bff7d8dcd2f9d4ae05653bbfa07e
data/.rubocop.yml CHANGED
@@ -12,6 +12,9 @@ Style/StringLiteralsInInterpolation:
12
12
  Metrics/BlockLength:
13
13
  Enabled: false
14
14
 
15
+ Metrics/ClassLength:
16
+ Enabled: false
17
+
15
18
  # affects similarity to other SDKs + pointless
16
19
  Metrics/MethodLength:
17
20
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
+ ## [1.2.0] - 2022-10-14
2
+
3
+ - Fixing polling client for server eval
4
+ - Added in tests for polling client
5
+ - Added in server eval for streaming client
6
+ - Added expired environment support for polling and streaming
7
+ - Added cache busting for server eval polling client
8
+ -
9
+
1
10
  ## [1.1.0] - 2022-10-12
2
11
 
3
12
  - Adds support for array values in flag evaluation contexts via [this PR](https://github.com/featurehub-io/featurehub-ruby-sdk/pull/12)
13
+
4
14
  ## [1.0.0] - 2022-06-06
5
15
 
6
16
  - Initial release, feature complete
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- featurehub-sdk (1.1.0)
4
+ featurehub-sdk (1.2.0)
5
5
  concurrent-ruby (~> 1.1.10)
6
6
  faraday (~> 2.3)
7
7
  ld-eventsource (~> 2.2.0)
@@ -74,7 +74,10 @@ module FeatureHub
74
74
  edge_provider
75
75
  end
76
76
 
77
- def use_polling_edge_service(interval = ENV.fetch("FEATUREHUB_POLL_INTERVAL", "30").to_i); end
77
+ def use_polling_edge_service(interval = ENV.fetch("FEATUREHUB_POLL_INTERVAL", "30").to_i)
78
+ @interval = interval
79
+ @edge_service_provider = method(:create_polling_edge_provider)
80
+ end
78
81
 
79
82
  def new_context
80
83
  get_or_create_edge_service
@@ -99,8 +102,11 @@ module FeatureHub
99
102
  @edge_service_provider.call(@repository, @api_keys, @edge_url, @logger)
100
103
  end
101
104
 
105
+ def create_polling_edge_provider(repo, api_keys, edge_url, logger)
106
+ FeatureHub::Sdk::PollingEdgeService.new(repo, api_keys, edge_url, @interval, logger)
107
+ end
108
+
102
109
  def create_default_provider(repo, api_keys, edge_url, logger)
103
- # FeatureHub::Sdk::PollingEdgeService.new(repo, api_keys, edge_url, 10, logger)
104
110
  FeatureHub::Sdk::StreamingEdgeService.new(repo, api_keys, edge_url, logger)
105
111
  end
106
112
 
@@ -85,7 +85,7 @@ module FeatureHub
85
85
  end
86
86
 
87
87
  def set?
88
- value != nil?
88
+ !value.nil?
89
89
  end
90
90
 
91
91
  def top_feature_state
@@ -4,12 +4,13 @@ require "faraday"
4
4
  require "faraday/net_http"
5
5
  require "json"
6
6
  require "concurrent-ruby"
7
+ require "digest/sha2"
7
8
 
8
9
  module FeatureHub
9
10
  module Sdk
10
11
  # uses a periodic polling mechanism to get updates
11
12
  class PollingEdgeService < EdgeService
12
- attr_reader :repository, :api_keys, :edge_url, :interval
13
+ attr_reader :repository, :api_keys, :edge_url, :interval, :stopped, :etag, :cancel, :sha_context
13
14
 
14
15
  def initialize(repository, api_keys, edge_url, interval, logger = nil)
15
16
  super(repository, api_keys, edge_url)
@@ -25,6 +26,8 @@ module FeatureHub
25
26
  @cancel = false
26
27
  @context = nil
27
28
  @etag = nil
29
+ @stopped = false
30
+ @sha_context = nil
28
31
 
29
32
  generate_url
30
33
  end
@@ -48,34 +51,53 @@ module FeatureHub
48
51
  return if new_header == @context
49
52
 
50
53
  @context = new_header
54
+ @sha_context = Digest::SHA256.hexdigest(@context)
51
55
 
52
- get_updates
56
+ if active
57
+ get_updates
58
+ else
59
+ poll
60
+ end
53
61
  end
54
62
 
55
63
  def close
56
64
  cancel_task
57
65
  end
58
66
 
67
+ def active
68
+ !@task.nil?
69
+ end
70
+
59
71
  private
60
72
 
61
73
  def poll_with_interval
62
- return if @cancel || !@task.nil?
63
-
64
- get_updates
74
+ return if @cancel || !@task.nil? || @stopped
65
75
 
66
76
  @logger.info("starting polling for #{determine_request_url}")
67
- @task = Concurrent::TimerTask.new(execution_interval: @interval) do
77
+ @task = Concurrent::TimerTask.new(execution_interval: @interval, run_now: false) do
68
78
  get_updates
69
79
  end
70
- @task.execute
80
+
81
+ get_updates
82
+
83
+ @task&.execute # could have been shutdown
71
84
  end
72
85
 
73
86
  def cancel_task
87
+ @cancel = true
88
+ shutdown_task
89
+ end
90
+
91
+ def stopped_task
92
+ @stopped = true
93
+ shutdown_task
94
+ end
95
+
96
+ def shutdown_task
74
97
  return if @task.nil?
75
98
 
76
99
  @task.shutdown
77
100
  @task = nil
78
- @cancel = true
79
101
  end
80
102
 
81
103
  # rubocop:disable Naming/AccessorMethodName
@@ -86,36 +108,63 @@ module FeatureHub
86
108
  "X-SDK": "Ruby",
87
109
  "X-SDK-Version": FeatureHub::Sdk::VERSION
88
110
  }
111
+
112
+ headers["x-featurehub"] = @context unless @context.nil?
89
113
  headers["if-none-match"] = @etag unless @etag.nil?
114
+
90
115
  @logger.debug("polling for #{url}")
91
- resp = @conn.get(url, request: { timeout: @timeout }, headers: headers)
116
+ resp = @conn.get url, {}, headers
92
117
  case resp.status
93
118
  when 200
94
- @etag = resp.headers["etag"]
95
- process_results(JSON.parse(resp.body))
119
+ success(resp)
120
+ when 236
121
+ stopped_task
122
+ success(resp)
96
123
  when 404 # no such key
97
124
  @repository.notify("failed", nil)
98
- @cancel = true
125
+ cancel_task
99
126
  @logger.error("featurehub: key does not exist, stopping polling")
100
127
  when 503 # dacha busy
101
- @logger.debug("featurehub: dacha is busy, trying tgaina")
128
+ @logger.debug("featurehub: dacha is busy, trying again")
102
129
  else
103
130
  @logger.debug("featurehub: unknown error #{resp.status}")
104
131
  end
105
132
  end
133
+
106
134
  # rubocop:enable Naming/AccessorMethodName
107
135
 
136
+ def success(resp)
137
+ @etag = resp.headers["etag"]
138
+
139
+ check_interval_change(resp.headers["cache-control"]) if resp.headers["cache-control"]
140
+
141
+ process_results(JSON.parse(resp.body))
142
+ end
143
+
108
144
  def process_results(data)
109
145
  data.each do |environment|
110
146
  @repository.notify("features", environment["features"]) if environment
111
147
  end
112
148
  end
113
149
 
150
+ def check_interval_change(cache_control_header)
151
+ found = cache_control_header.scan(/max-age=(\d+)/)
152
+
153
+ return if @task.nil? || found.empty? || found[0].empty?
154
+
155
+ new_interval = found[0][0].to_i
156
+
157
+ return unless new_interval.positive? && new_interval != @interval
158
+
159
+ @interval = new_interval
160
+ @task.execution_interval = @interval
161
+ end
162
+
114
163
  def determine_request_url
115
164
  if @context.nil?
116
- @url
165
+ "#{@url}&contextSha=0"
117
166
  else
118
- "#{@url}&#{@context}"
167
+ "#{@url}&contextSha=#{@sha_context}"
119
168
  end
120
169
  end
121
170
 
@@ -125,6 +174,7 @@ module FeatureHub
125
174
  @timeout = ENV.fetch("FEATUREHUB_POLL_HTTP_TIMEOUT", "12").to_i
126
175
  @conn = Faraday.new(url: @edge_url) do |f|
127
176
  f.adapter :net_http
177
+ f.options.timeout = @timeout
128
178
  end
129
179
  end
130
180
  end
@@ -7,7 +7,7 @@ module FeatureHub
7
7
  module Sdk
8
8
  # provides a streaming service
9
9
  class StreamingEdgeService < FeatureHub::Sdk::EdgeService
10
- attr_reader :repository, :sse_client, :url, :closed
10
+ attr_reader :repository, :sse_client, :url, :stopped
11
11
 
12
12
  def initialize(repository, api_keys, edge_url, logger = nil)
13
13
  super(repository, api_keys, edge_url)
@@ -15,42 +15,78 @@ module FeatureHub
15
15
  @url = "#{edge_url}features/#{api_keys[0]}"
16
16
  @repository = repository
17
17
  @sse_client = nil
18
- @closed = true
18
+ @context = nil
19
19
  @logger = logger || FeatureHub::Sdk.default_logger
20
20
  end
21
21
 
22
+ def closed
23
+ @sse_client.nil?
24
+ end
25
+
22
26
  def poll
23
- start_streaming unless @sse_client
27
+ start_streaming unless @sse_client || @stopped
24
28
  end
25
29
 
26
30
  def active
27
- !@closed && !@sse_client.nil?
31
+ !@sse_client.nil?
28
32
  end
29
33
 
30
34
  def close
31
- @closed = true
35
+ close_connection
36
+ end
37
+
38
+ private
39
+
40
+ def close_connection
32
41
  return if @sse_client.nil?
33
42
 
34
43
  @sse_client.close
35
44
  @sse_client = nil
36
45
  end
37
46
 
38
- private
47
+ def stop
48
+ @stopped = true
49
+ close_connection
50
+ end
51
+
52
+ def context_change(new_header)
53
+ return if new_header == @context
54
+
55
+ @context = new_header
56
+ close
57
+ poll
58
+ end
39
59
 
40
60
  def start_streaming
41
- @closed = false
42
61
  @logger.info("streaming from #{@url}")
62
+ # we can get an error before returning the new() function and get a race condition on the close
63
+ must_close = false
43
64
  @sse_client = SSE::Client.new(@url) do |client|
44
65
  client.on_event do |event|
45
- @repository.notify(event.type, JSON.parse(event.data))
66
+ json_data = JSON.parse(event.data)
67
+
68
+ if event.type == "config"
69
+ process_config(json_data)
70
+ else
71
+ @repository.notify(event.type, json_data)
72
+ end
46
73
  end
47
74
  client.on_error do |error|
48
75
  if error.is_a?(SSE::Errors::HTTPStatusError) && (error.status == 404)
49
76
  @repository.notify("failure", nil)
50
77
  close
78
+ must_close = true
51
79
  end
52
80
  end
53
81
  end
82
+
83
+ return unless must_close
84
+
85
+ close # try again
86
+ end
87
+
88
+ def process_config(json_data)
89
+ stop if json_data["edge.stale"]
54
90
  end
55
91
  end
56
92
  end
@@ -3,7 +3,7 @@
3
3
  module FeatureHub
4
4
  # already documented elsewhere
5
5
  module Sdk
6
- VERSION = "1.1.0"
6
+ VERSION = "1.2.1"
7
7
 
8
8
  def default_logger
9
9
  log = ::Logger.new($stdout)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featurehub-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Vowles
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-10-13 00:00:00.000000000 Z
12
+ date: 2022-11-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: concurrent-ruby
@@ -97,7 +97,6 @@ files:
97
97
  - Gemfile.lock
98
98
  - LICENSE.txt
99
99
  - Rakefile
100
- - featurehub-sdk.gemspec
101
100
  - featurehub-sdk.iml
102
101
  - lib/feature_hub/sdk/context.rb
103
102
  - lib/feature_hub/sdk/feature_hub_config.rb
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path("lib", __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "feature_hub/sdk/version"
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = "featurehub-sdk"
9
- spec.version = FeatureHub::Sdk::VERSION
10
- spec.authors = ["Richard Vowles", "Irina Southwell"]
11
- spec.email = ["richard@bluetrainsoftware.com"]
12
-
13
- spec.summary = "FeatureHub Ruby SDK"
14
- spec.description = "FeatureHub Ruby SDK"
15
- spec.homepage = "https://www.featurehub.io"
16
- spec.license = "MIT"
17
- spec.required_ruby_version = ">= 2.6.0"
18
-
19
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
-
21
- spec.metadata["homepage_uri"] = spec.homepage
22
- spec.metadata["source_code_uri"] = "https://github.com/featurehub-io/featurehub-ruby-sdk"
23
- spec.metadata["changelog_uri"] = "https://github.com/featurehub-io/featurehub-ruby-sdk/featurehub-sdk/CHANGELOG.md"
24
- spec.metadata["rubygems_mfa_required"] = "true"
25
-
26
- # Specify which files should be added to the gem when it is released.
27
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
- `git ls-files -z`.split("\x0").reject do |f|
30
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
31
- end
32
- end
33
- spec.bindir = "exe"
34
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
- spec.require_paths = ["lib"]
36
-
37
- spec.add_dependency "concurrent-ruby", "~> 1.1.10"
38
- spec.add_dependency "faraday", "~> 2.3"
39
- spec.add_dependency "ld-eventsource", "~> 2.2.0"
40
- spec.add_dependency "murmurhash3", "~> 0.1.6"
41
- spec.add_dependency "sem_version", "~> 2.0.0"
42
-
43
- # For more information and examples about making a new gem, check out our
44
- # guide at: https://bundler.io/guides/creating_gem.html
45
- end