devcycle-ruby-server-sdk 2.7.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/devcycle-ruby-server-sdk.gemspec +1 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb +103 -19
- 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/version.rb +1 -1
- 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: 11f97c726357c0e8c267da73d94ed9c1a24e290f6bb4340e337f0b3bb78b66e3
|
4
|
+
data.tar.gz: 9554c5c27297236f1e3e588ff1e15c43090a042807afb44ef239052e5ae81bbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41e832272e313529bd070d3b0aeb909e166114a451ee4493afa60d432b979adcc996198f9ae4a95407056512df144d138362738da1d9bf5033778312440c3ebb
|
7
|
+
data.tar.gz: 93127946368ce2350cf0dac32d7928b5d6a3c49f9c6fca6e69dd82b10e6b7c70b12a8a0227ce68acff783ae02296eccd97ad4d59e6ce760a9474879b27087dd1
|
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'
|
@@ -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
|
@@ -19,15 +20,18 @@ module DevCycle
|
|
19
20
|
@config_version = "v1"
|
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"
|
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:
|
4
|
+
version: 3.1.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-20 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
|