devcycle-ruby-server-sdk 3.0.0 → 3.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: 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