devcycle-ruby-server-sdk 3.0.0 → 3.2.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
  SHA256:
3
- metadata.gz: 2f9dd2899f5a8f286a66a667fa27623331572f5c35dec9ad363ab0be2e191296
4
- data.tar.gz: e0971e7a1bdafc366b2021df303dbfc0f30f230d1c353e435d810805026ab866
3
+ metadata.gz: a00494492e31ee9cbd1aeb791c0372402ab5a243717e2d14e5cae6b16b41e639
4
+ data.tar.gz: 8dc6223226365eaca06e35cbdc6ed7006b190e01d378279b3c6a9e22d519b4f4
5
5
  SHA512:
6
- metadata.gz: 0c2d2b26b6ef08f548c58ecfbca13b2a09ab5643184f2018f9ede3e41ca1a115f17024f577e84ce12c27a23161be3cb7dc0d717569d23f4698b1f0398847dfdf
7
- data.tar.gz: 1bfe5568cc56cf86566c9fe9d7480ffb6b3f7edf6907544a3305e99fbe64a163a867aa8f713015172591673889bd688fd81971efd4fbb91ef152433c59637b61
6
+ metadata.gz: b51d40ce54ccfc9b21742f888877f2522ed464f676aa90016ff4fd2c8d6b12773b8c2eff782c97b4226669a815bc07d0ac77f715b7bfd6d8005b3bf327e60238
7
+ data.tar.gz: b15efd1fddb5c34a79bc6ffba5dbe16fd7e139e8f0acb75f917ca13696b3ff6787bf09e5fd5d57de4c36848e75ca9694b978d5b83f9746f631291b79b861e82a
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gem 'oj'
6
6
  gem 'wasmtime'
7
7
  gem 'concurrent-ruby'
8
8
  gem 'google-protobuf'
9
+ gem 'ld-eventsource'
9
10
 
10
11
  group :development, :test do
11
12
  gem 'sorbet'
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_runtime_dependency 'sorbet-runtime', '>= 0.5.11481'
30
30
  s.add_runtime_dependency 'oj', '~> 3.0'
31
31
  s.add_runtime_dependency 'google-protobuf', '~> 3.22'
32
+ s.add_runtime_dependency 'ld-eventsource', '~> 2.2.2'
32
33
 
33
34
 
34
35
  s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
@@ -5,6 +5,7 @@ require 'concurrent-ruby'
5
5
  require 'typhoeus'
6
6
  require 'json'
7
7
  require 'time'
8
+ require 'ld-eventsource'
8
9
 
9
10
  module DevCycle
10
11
  class ConfigManager
@@ -16,18 +17,21 @@ module DevCycle
16
17
  ).void }
17
18
  def initialize(sdkKey, local_bucketing, wait_for_init)
18
19
  @first_load = true
19
- @config_version = "v1"
20
+ @config_version = "v2"
20
21
  @local_bucketing = local_bucketing
21
22
  @sdkKey = sdkKey
23
+ @sse_url = ""
24
+ @sse_polling = false
22
25
  @config_e_tag = ""
23
26
  @config_last_modified = ""
24
27
  @logger = local_bucketing.options.logger
28
+ @enable_sse = local_bucketing.options.enable_beta_realtime_updates
25
29
  @polling_enabled = true
30
+ @sse_active = false
26
31
  @max_config_retries = 2
27
-
28
32
  @config_poller = Concurrent::TimerTask.new({
29
33
  execution_interval: @local_bucketing.options.config_polling_interval_ms.fdiv(1000)
30
- }) do |task|
34
+ }) do |_|
31
35
  fetch_config
32
36
  end
33
37
 
@@ -38,7 +42,7 @@ module DevCycle
38
42
  def initialize_config
39
43
  begin
40
44
  fetch_config
41
- @config_poller.execute if @polling_enabled
45
+ start_polling(false)
42
46
  rescue => e
43
47
  @logger.error("DevCycle: Error Initializing Config: #{e.message}")
44
48
  ensure
@@ -46,8 +50,8 @@ module DevCycle
46
50
  end
47
51
  end
48
52
 
49
- def fetch_config
50
- return unless @polling_enabled
53
+ def fetch_config(min_last_modified: -1)
54
+ return unless @polling_enabled || (@sse_active && @enable_sse)
51
55
 
52
56
  req = Typhoeus::Request.new(
53
57
  get_config_url,
@@ -56,14 +60,24 @@ module DevCycle
56
60
  })
57
61
 
58
62
  begin
59
- Date.parse(@config_last_modified)
63
+ # Blind parse the lastmodified string to check if it's a valid date.
64
+ # This short circuits the rest of the checks if it's not set
60
65
  if @config_last_modified != ""
61
- req.options[:headers]["If-Modified-Since"] = Time.httpdate(@config_last_modified)
66
+ if min_last_modified != -1
67
+ stored_date = Date.parse(@config_last_modified)
68
+ parsed_sse_ts = Time.at(min_last_modified)
69
+ if parsed_sse_ts.utc > stored_date.utc
70
+ req.options[:headers]["If-Modified-Since"] = parsed_sse_ts.utc.httpdate
71
+ else
72
+ req.options[:headers]["If-Modified-Since"] = @config_last_modified
73
+ end
74
+ else
75
+ req.options[:headers]["If-Modified-Since"] = @config_last_modified
76
+ end
62
77
  end
63
78
  rescue
64
79
  end
65
80
 
66
-
67
81
  if @config_e_tag != ""
68
82
  req.options[:headers]['If-None-Match'] = @config_e_tag
69
83
  end
@@ -77,28 +91,33 @@ module DevCycle
77
91
  @logger.debug("Config not modified, using cache, etag: #{@config_e_tag}, last modified: #{@config_last_modified}")
78
92
  break
79
93
  when 200
80
- @logger.debug("New config received, etag: #{resp.headers['Etag']} LM:#{resp.headers['Last-Modified']}")
81
- lm_header = resp.headers['Last-Modified']
94
+ @logger.debug("New config received, etag: #{resp.headers['Etag']} LastModified:#{resp.headers['Last-Modified']}")
82
95
  begin
83
- lm_timestamp = Time.rfc2822(lm_header)
96
+ if @config_last_modified == ""
97
+ set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
98
+ return
99
+ end
100
+
101
+ lm_timestamp = Time.rfc2822(resp.headers['Last-Modified'])
84
102
  current_lm = Time.rfc2822(@config_last_modified)
85
103
  if lm_timestamp == "" && @config_last_modified == "" || (current_lm.utc < lm_timestamp.utc)
86
- set_config(resp.body, resp.headers['Etag'], lm_header)
87
- @logger.debug("New config stored, etag: #{@config_e_tag}, last modified: #{lm_header}")
104
+ set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
88
105
  else
89
- @logger.warn("Config response was an older config than currently stored config.")
106
+ @logger.warn("Config last-modified was an older date than currently stored config.")
90
107
  end
91
108
  rescue
92
- @logger.warn("Failed to parse last modified header, setting config.")
93
- set_config(resp.body, resp.headers['Etag'], lm_header)
109
+ @logger.warn("Failed to parse last modified header, setting config anyway.")
110
+ set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
94
111
  end
95
112
  break
96
113
  when 403
97
114
  stop_polling
115
+ stop_sse
98
116
  @logger.error("Failed to download DevCycle config; Invalid SDK Key.")
99
117
  break
100
118
  when 404
101
119
  stop_polling
120
+ stop_sse
102
121
  @logger.error("Failed to download DevCycle config; Config not found.")
103
122
  break
104
123
  when 500...599
@@ -110,7 +129,6 @@ module DevCycle
110
129
  break
111
130
  end
112
131
  end
113
-
114
132
  nil
115
133
  end
116
134
 
@@ -118,11 +136,21 @@ module DevCycle
118
136
  if !JSON.parse(config).is_a?(Hash)
119
137
  raise("Invalid JSON body parsed from Config Response")
120
138
  end
121
-
139
+ parsed_config = JSON.parse(config)
140
+
141
+ if parsed_config['sse'] != nil
142
+ raw_url = "#{parsed_config['sse']['hostname']}#{parsed_config['sse']['path']}"
143
+ if @sse_url != raw_url && raw_url != ""
144
+ stop_sse
145
+ @sse_url = raw_url
146
+ init_sse(@sse_url)
147
+ end
148
+ end
122
149
  @local_bucketing.store_config(config)
123
150
  @config_e_tag = etag
124
151
  @config_last_modified = lastmodified
125
152
  @local_bucketing.has_config = true
153
+ @logger.debug("New config stored, etag: #{@config_e_tag}, last modified: #{@config_last_modified}")
126
154
  end
127
155
 
128
156
  def get_config_url
@@ -130,14 +158,70 @@ module DevCycle
130
158
  "#{configBasePath}/config/#{@config_version}/server/#{@sdkKey}.json"
131
159
  end
132
160
 
133
- def stop_polling
161
+ def start_polling(sse)
162
+ if sse
163
+ @config_poller.shutdown if @config_poller.running?
164
+ @config_poller = Concurrent::TimerTask.new({ execution_interval: 60 * 10 }) do |_|
165
+ fetch_config
166
+ end
167
+ @sse_polling = sse
168
+ end
169
+ @polling_enabled = true
170
+ @config_poller.execute if @polling_enabled && (!@sse_active || sse)
171
+ end
172
+
173
+ def stop_polling()
134
174
  @polling_enabled = false
135
175
  @config_poller.shutdown if @config_poller.running?
136
176
  end
137
177
 
178
+ def stop_sse
179
+ return unless @enable_sse
180
+ @sse_active = false
181
+ @sse_polling = false
182
+ @sse_client.close if @sse_client
183
+ start_polling(@sse_polling)
184
+ end
185
+
138
186
  def close
139
187
  @config_poller.shutdown if @config_poller.running?
140
188
  nil
141
189
  end
190
+
191
+ def init_sse(path)
192
+ return unless @enable_sse
193
+ @logger.debug("Initializing SSE with url: #{path}")
194
+ @sse_active = true
195
+ @sse_client = SSE::Client.new(path) do |client|
196
+ client.on_event do |event|
197
+
198
+ parsed_json = JSON.parse(event.data)
199
+ handle_sse(parsed_json)
200
+ end
201
+ client.on_error do |error|
202
+ @logger.debug("SSE Error: #{error.message}")
203
+ end
204
+ end
205
+ end
206
+
207
+ def handle_sse(event_data)
208
+ unless @sse_polling
209
+ stop_polling
210
+ start_polling(true)
211
+ end
212
+ if event_data["data"] == nil
213
+ return
214
+ end
215
+ @logger.debug("SSE: Message received: #{event_data["data"]}")
216
+ parsed_event_data = JSON.parse(event_data["data"])
217
+
218
+ last_modified = parsed_event_data["lastModified"]
219
+ event_type = parsed_event_data["type"]
220
+
221
+ if event_type == "refetchConfig" || event_type == nil
222
+ @logger.debug("SSE: Re-fetching new config with TS: #{last_modified}")
223
+ fetch_config(min_last_modified: last_modified / 1000)
224
+ end
225
+ end
142
226
  end
143
227
  end
@@ -26,6 +26,7 @@ module DevCycle
26
26
  @flush_mutex = Mutex.new
27
27
  @local_bucketing = local_bucketing
28
28
  @local_bucketing.init_event_queue(@client_uuid, options)
29
+
29
30
  end
30
31
 
31
32
  def close
@@ -3,6 +3,7 @@ module DevCycle
3
3
  attr_reader :config_polling_interval_ms
4
4
  attr_reader :enable_edge_db
5
5
  attr_reader :enable_cloud_bucketing
6
+ attr_reader :enable_beta_realtime_updates
6
7
  attr_reader :config_cdn_uri
7
8
  attr_reader :events_api_uri
8
9
  attr_reader :bucketing_api_uri
@@ -14,6 +15,7 @@ module DevCycle
14
15
  disable_custom_event_logging: false,
15
16
  disable_automatic_event_logging: false,
16
17
  config_polling_interval_ms: 10_000,
18
+ enable_beta_realtime_updates: false,
17
19
  request_timeout_ms: 5_000,
18
20
  max_event_queue_size: 2_000,
19
21
  flush_event_queue_size: 1_000,
@@ -71,6 +73,7 @@ module DevCycle
71
73
 
72
74
  @disable_custom_event_logging = disable_custom_event_logging
73
75
  @disable_automatic_event_logging = disable_automatic_event_logging
76
+ @enable_beta_realtime_updates = enable_beta_realtime_updates
74
77
  @config_cdn_uri = config_cdn_uri
75
78
  @events_api_uri = events_api_uri
76
79
  @bucketing_api_uri = "https://bucketing-api.devcyle.com"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- BUCKETING_LIB_VERSION="1.24.2"
2
+ BUCKETING_LIB_VERSION="1.25.3"
3
3
  WAT_DOWNLOAD=0
4
4
  rm bucketing-lib.release.wasm
5
5
  wget "https://unpkg.com/@devcycle/bucketing-assembly-script@$BUCKETING_LIB_VERSION/build/bucketing-lib.release.wasm"
@@ -11,5 +11,5 @@ OpenAPI Generator version: 5.3.0
11
11
  =end
12
12
 
13
13
  module DevCycle
14
- VERSION = '3.0.0'
14
+ VERSION = '3.2.0'
15
15
  end
@@ -18,9 +18,14 @@ require 'json'
18
18
  # Please update as you see appropriate
19
19
  describe 'DevCycle::Client' do
20
20
  before(:all) do
21
+ sdk_key = ENV["DEVCYCLE_SERVER_SDK_KEY"]
22
+ if sdk_key.nil?
23
+ puts("SDK KEY NOT SET - SKIPPING INIT")
24
+ return
25
+ end
21
26
  # run before each test
22
27
  options = DevCycle::Options.new(enable_cloud_bucketing: true)
23
- @api_instance = DevCycle::Client.new("dvc_server_token_hash", options)
28
+ @api_instance = DevCycle::Client.new(sdk_key, options)
24
29
 
25
30
  @user = DevCycle::User.new({
26
31
  user_id: 'test-user',
@@ -57,12 +62,12 @@ describe 'DevCycle::Client' do
57
62
  # @param user
58
63
  # @param [Hash] opts the optional parameters
59
64
  # @return [Variable]
60
- describe 'get_variable_by_key activate-flag' do
65
+ describe 'get_variable_by_key ruby-example-tests' do
61
66
  it 'should work' do
62
- result = @api_instance.variable(@user, "activate-flag", false)
67
+ result = @api_instance.variable(@user, "ruby-example-tests-default", false)
63
68
  expect(result.isDefaulted).to eq true
64
69
 
65
- result = @api_instance.variable_value(@user, "activate-flag", true)
70
+ result = @api_instance.variable_value(@user, "ruby-example-tests-default", true)
66
71
  expect(result).to eq true
67
72
  end
68
73
  end
@@ -75,11 +80,11 @@ describe 'DevCycle::Client' do
75
80
  # @return [Variable]
76
81
  describe 'get_variable_by_key test' do
77
82
  it 'should work' do
78
- result = @api_instance.variable(@user, "test", false)
83
+ result = @api_instance.variable(@user, "ruby-example-tests", false)
79
84
  expect(result.isDefaulted).to eq false
80
85
  expect(result.value).to eq true
81
86
 
82
- result = @api_instance.variable_value(@user, "test", true)
87
+ result = @api_instance.variable_value(@user, "ruby-example-tests", true)
83
88
  expect(result).to eq true
84
89
  end
85
90
  end
@@ -93,7 +98,7 @@ describe 'DevCycle::Client' do
93
98
  it 'should work' do
94
99
  result = @api_instance.all_variables(@user)
95
100
 
96
- expect(result.length).to eq 5
101
+ expect(result.length >= 1).to eq true
97
102
  end
98
103
  end
99
104
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devcycle-ruby-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DevCycleHQ
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-17 00:00:00.000000000 Z
11
+ date: 2024-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -100,6 +100,20 @@ dependencies:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
102
  version: '3.22'
103
+ - !ruby/object:Gem::Dependency
104
+ name: ld-eventsource
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 2.2.2
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 2.2.2
103
117
  - !ruby/object:Gem::Dependency
104
118
  name: rspec
105
119
  requirement: !ruby/object:Gem::Requirement