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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75a9de78ddc4e249767ad074f6e2a8021b4705bb8f99e54202b774fffee4d8e5
4
- data.tar.gz: df1c02d526d99595cebc42dddbc4952e2e917bc22df68e695d863eb6fa226fe5
3
+ metadata.gz: 11f97c726357c0e8c267da73d94ed9c1a24e290f6bb4340e337f0b3bb78b66e3
4
+ data.tar.gz: 9554c5c27297236f1e3e588ff1e15c43090a042807afb44ef239052e5ae81bbd
5
5
  SHA512:
6
- metadata.gz: d1b4943aabe48858c247a1426fe3b06399f09d5c680f15eec953188fb5dece618adab305563eb5d3ef2bfc75b59b0dd74a6ac6ff820842d0b14796c2fa2e242b
7
- data.tar.gz: ecae14540e9316e5e182dae5dc27e60831241ba91905e9c8d962f58c8ca8736f1df863f2ed5832458c3a0edb0e6241fd245fbb9415b0e61b7678b0f4fffcc018
6
+ metadata.gz: 41e832272e313529bd070d3b0aeb909e166114a451ee4493afa60d432b979adcc996198f9ae4a95407056512df144d138362738da1d9bf5033778312440c3ebb
7
+ data.tar.gz: 93127946368ce2350cf0dac32d7928b5d6a3c49f9c6fca6e69dd82b10e6b7c70b12a8a0227ce68acff783ae02296eccd97ad4d59e6ce760a9474879b27087dd1
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
@@ -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 |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"
@@ -11,5 +11,5 @@ OpenAPI Generator version: 5.3.0
11
11
  =end
12
12
 
13
13
  module DevCycle
14
- VERSION = '2.7.1'
14
+ VERSION = '3.1.0'
15
15
  end
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: 2.7.1
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-07-17 00:00:00.000000000 Z
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