featurehub-sdk 1.1.0 → 1.2.0

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
  SHA256:
3
- metadata.gz: c4621f1ab8e515ab8eec3b701bf3928d1eabfafbe21a955175729dfeb39ed31b
4
- data.tar.gz: 3e57a7dc1e0e525e6da0dca8848d42fd09eea437a378162160b5e529422fa12c
3
+ metadata.gz: 033fbbd756fd2a748d584634724881aae1a2d49feec0a2f7cec4aa7245dc8440
4
+ data.tar.gz: 042ff7ee449a8b80a5f120d203c791e7e42c896dce377a5c7af4367c94048829
5
5
  SHA512:
6
- metadata.gz: 95899cd71c1c70756cddc97821c30173ef5381c0ba384d58e9be0b6c57664b59be85e4f748d55723677b84dfdda276df48526a8bb603165da911af1fc0344305
7
- data.tar.gz: 8840b4aa7359fa5bbbc3247bac92df4fc96915c788737d08e3d4a0ccd92542536e05b527411875f17b4a5b94b7614f033303eaba93d349a88be200e010f8ccef
6
+ metadata.gz: 0fe881017cbb21138eed0663f0df11d18df61e63be666c5e2e26b68ca478792e8bb51f123ea415d86c8ce6ede3e087b2c7e8b35991c90d6939bac7672f0f7de4
7
+ data.tar.gz: 944146e27443caad934f35b1efb30179ec3c1a8b8c78d2def46e68250b338b156184e79b861be2ae3b929cf4b38a7514978a1757bd7e24a65ede50930010c9a7
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
 
@@ -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.0"
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.0
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-04 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