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 +4 -4
- data/Gemfile +1 -0
- data/devcycle-ruby-server-sdk.gemspec +1 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm +0 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb +104 -20
- data/lib/devcycle-ruby-server-sdk/localbucketing/event_queue.rb +1 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/options.rb +3 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/update_wasm.sh +1 -1
- data/lib/devcycle-ruby-server-sdk/version.rb +1 -1
- data/spec/api/devcycle_api_spec.rb +12 -7
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a00494492e31ee9cbd1aeb791c0372402ab5a243717e2d14e5cae6b16b41e639
|
4
|
+
data.tar.gz: 8dc6223226365eaca06e35cbdc6ed7006b190e01d378279b3c6a9e22d519b4f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b51d40ce54ccfc9b21742f888877f2522ed464f676aa90016ff4fd2c8d6b12773b8c2eff782c97b4226669a815bc07d0ac77f715b7bfd6d8005b3bf327e60238
|
7
|
+
data.tar.gz: b15efd1fddb5c34a79bc6ffba5dbe16fd7e139e8f0acb75f917ca13696b3ff6787bf09e5fd5d57de4c36848e75ca9694b978d5b83f9746f631291b79b861e82a
|
data/Gemfile
CHANGED
@@ -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'
|
Binary file
|
@@ -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 = "
|
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 |
|
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
|
-
|
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
|
-
|
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
|
-
|
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']}
|
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
|
-
|
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'],
|
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
|
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'],
|
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
|
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
|
@@ -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"
|
@@ -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(
|
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
|
65
|
+
describe 'get_variable_by_key ruby-example-tests' do
|
61
66
|
it 'should work' do
|
62
|
-
result = @api_instance.variable(@user, "
|
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, "
|
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, "
|
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, "
|
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
|
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.
|
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-
|
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
|