devcycle-ruby-server-sdk 1.2.0 → 2.0.1

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -0
  3. data/devcycle-ruby-server-sdk.gemspec +9 -1
  4. data/lib/devcycle-ruby-server-sdk/api/devcycle_api.rb +128 -21
  5. data/lib/devcycle-ruby-server-sdk/api_client.rb +1 -1
  6. data/lib/devcycle-ruby-server-sdk/configuration.rb +5 -5
  7. data/lib/devcycle-ruby-server-sdk/localbucketing/bucketed_user_config.rb +21 -0
  8. data/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm +0 -0
  9. data/lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb +97 -0
  10. data/lib/devcycle-ruby-server-sdk/localbucketing/dvc_options.rb +119 -0
  11. data/lib/devcycle-ruby-server-sdk/localbucketing/event_queue.rb +120 -0
  12. data/lib/devcycle-ruby-server-sdk/localbucketing/event_types.rb +8 -0
  13. data/lib/devcycle-ruby-server-sdk/localbucketing/events_payload.rb +23 -0
  14. data/lib/devcycle-ruby-server-sdk/localbucketing/local_bucketing.rb +257 -0
  15. data/lib/devcycle-ruby-server-sdk/localbucketing/platform_data.rb +29 -0
  16. data/lib/devcycle-ruby-server-sdk/models/event.rb +6 -6
  17. data/lib/devcycle-ruby-server-sdk/models/user_data.rb +78 -107
  18. data/lib/devcycle-ruby-server-sdk/models/variable.rb +0 -13
  19. data/lib/devcycle-ruby-server-sdk/version.rb +1 -1
  20. data/lib/devcycle-ruby-server-sdk.rb +9 -0
  21. data/spec/api/devcycle_api_spec.rb +26 -37
  22. metadata +77 -25
  23. data/Gemfile.lock +0 -70
  24. data/Rakefile +0 -10
  25. data/docs/DevcycleApi.md +0 -290
  26. data/docs/ErrorResponse.md +0 -20
  27. data/docs/Event.md +0 -26
  28. data/docs/Feature.md +0 -26
  29. data/docs/UserData.md +0 -48
  30. data/docs/Variable.md +0 -24
  31. data/examples/sinatra/Gemfile +0 -8
  32. data/examples/sinatra/Gemfile.lock +0 -51
  33. data/examples/sinatra/README.md +0 -14
  34. data/examples/sinatra/app.rb +0 -48
  35. data/git_push.sh +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b37103165cc47272d718527f01a20fd41e5a4fa851329a1b9ae472d04c561e87
4
- data.tar.gz: dc33b920301e236e1f4829c3c5b2bfa811c4c7649146d59719aefb81111bded1
3
+ metadata.gz: fc0fd57c7d492253c2a5aed803845d028057f5075f33ed40b7cd31d6862bd7b2
4
+ data.tar.gz: 3cf436a16d4ae282192e77687fccaf17d3346b1facb364a26f4ae932ed658140
5
5
  SHA512:
6
- metadata.gz: bd9a35095127bfd7e51a9af0894b7d0244e9d8f494566a85b95b71179c1090f79cf4d757d427a98553aaccbc90bbcb1cd8606f0001b3b09f36dedc3629286588
7
- data.tar.gz: 5ca612fe98bad4539f5b5d87ac1b3512d0eca2fbb285a3482d5675c1b4cfd52643bd94ff3dbebca585cf5f288e8bb08d7bee5ccee869b45d27e44c08bd472116
6
+ metadata.gz: 8e463d4849b50cfd4a346e252909d968f2a41ed31e3bac91de33ad968ecca3afd0ceefb1243cb3326049853894502f8979cd8574fa81bc12cbc9cf66ea34edfb
7
+ data.tar.gz: f9467f2c7ad852e18b9de9bba1a96a73b55febe9ab25d64571e09e19ab9dac394a965d543f77c3aed44a4a1719368c677151d016ca8ba0c0772d2592a78489cf
data/Gemfile CHANGED
@@ -1,8 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+ gem 'sorbet-runtime'
5
+ gem 'oj'
6
+ gem 'wasmtime'
7
+ gem 'concurrent-ruby'
4
8
 
5
9
  group :development, :test do
10
+ gem 'sorbet'
11
+ gem 'tapioca', require: false
6
12
  gem 'rake', '~> 13.0.1'
7
13
  gem 'pry-byebug'
8
14
  gem 'rubocop', '~> 0.66.0'
@@ -24,10 +24,18 @@ Gem::Specification.new do |s|
24
24
  s.required_ruby_version = ">= 2.4"
25
25
 
26
26
  s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
27
+ s.add_runtime_dependency 'wasmtime', '5.0.0'
28
+ s.add_runtime_dependency 'concurrent-ruby', '1.2.0'
29
+ s.add_runtime_dependency 'sorbet-runtime', '0.5.10648'
30
+ s.add_runtime_dependency 'oj', '3.13.2'
31
+
27
32
 
28
33
  s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
29
34
 
30
- s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? }
35
+ s.files = Dir['README.md', 'LICENSE',
36
+ 'lib/**/*',
37
+ 'devcycle-ruby-server-sdk.gemspec',
38
+ 'Gemfile']
31
39
  s.test_files = `find spec/*`.split("\n")
32
40
  s.executables = []
33
41
  s.require_paths = ["lib"]
@@ -11,13 +11,58 @@ OpenAPI Generator version: 5.3.0
11
11
  =end
12
12
 
13
13
  require 'cgi'
14
+ require 'logger'
14
15
 
15
16
  module DevCycle
16
17
  class DVCClient
17
- attr_accessor :api_client
18
+ def initialize(sdkKey, dvc_options = DVCOptions.new, wait_for_init = false)
19
+ if sdkKey.nil?
20
+ raise ArgumentError('Missing SDK key!')
21
+ elsif !sdkKey.start_with?('server') && !sdkKey.start_with?('dvc_server')
22
+ raise ArgumentError('Invalid SDK key!')
23
+ end
24
+
25
+ @sdkKey = sdkKey
26
+ @dvc_options = dvc_options
27
+ @logger = dvc_options.logger
28
+
29
+ if @dvc_options.enable_cloud_bucketing
30
+ @api_client = ApiClient.default
31
+ @api_client.config.api_key['bearerAuth'] = @sdkKey
32
+ @api_client.config.enable_edge_db = @dvc_options.enable_edge_db
33
+ @api_client.config.logger = @logger
34
+ else
35
+ @localbucketing = LocalBucketing.new(@sdkKey, dvc_options, wait_for_init)
36
+ @event_queue = EventQueue.new(@sdkKey, dvc_options.event_queue_options, @localbucketing)
37
+ end
38
+ end
39
+
40
+ def close
41
+ if @dvc_options.enable_cloud_bucketing
42
+ @logger.info("Cloud Bucketing does not require closing.")
43
+ return
44
+ end
45
+ if @localbucketing != nil
46
+ if !@localbucketing.initialized
47
+ @logger.info("Awaiting client initialization before closing")
48
+ while !@localbucketing.initialized
49
+ sleep(0.5)
50
+ end
51
+ end
52
+ @localbucketing.close
53
+ @localbucketing = nil
54
+ @logger.info("Closed DevCycle Local Bucketing Engine.")
55
+ end
56
+
57
+ @event_queue.close
58
+ @logger.info("Closed DevCycle Client.")
59
+ end
18
60
 
19
- def initialize(api_client = ApiClient.default)
20
- @api_client = api_client
61
+ def set_client_custom_data(customdata)
62
+ if @api_client.config.enable_cloud_bucketing
63
+ fail ArgumentError("Client Custom Data is only available in Local bucketing mode.")
64
+ end
65
+ @localbucketing.set_client_custom_data(customdata)
21
66
  end
22
67
 
23
68
  def validate_model(model)
@@ -36,8 +81,17 @@ module DevCycle
36
81
 
37
82
  validate_model(user_data)
38
83
 
39
- data, _status_code, _headers = all_features_with_http_info(user_data, opts)
40
- data
84
+ if @dvc_options.enable_cloud_bucketing
85
+ data, _status_code, _headers = all_features_with_http_info(user_data, opts)
86
+ return data
87
+ end
88
+
89
+ if local_bucketing_initialized?
90
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
91
+ bucketed_config.features
92
+ else
93
+ {}
94
+ end
41
95
  end
42
96
 
43
97
  # Get all features by key for user data
@@ -65,14 +119,14 @@ module DevCycle
65
119
  # HTTP header 'Content-Type'
66
120
  content_type = @api_client.select_header_content_type(['application/json'])
67
121
  if !content_type.nil?
68
- header_params['Content-Type'] = content_type
122
+ header_params['Content-Type'] = content_type
69
123
  end
70
124
 
71
125
  # form parameters
72
126
  form_params = opts[:form_params] || {}
73
127
 
74
128
  # http body (model)
75
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
129
+ post_body = opts[:debug_body] || user_data.to_json
76
130
 
77
131
  # return_type
78
132
  return_type = opts[:debug_return_type] || 'Hash<String, Feature>'
@@ -110,8 +164,36 @@ module DevCycle
110
164
 
111
165
  validate_model(user_data)
112
166
 
113
- data, _status_code, _headers = variable_with_http_info(key, user_data, default, opts)
114
- data
167
+ if @dvc_options.enable_cloud_bucketing
168
+ data, _status_code, _headers = variable_with_http_info(key, user_data, default, opts)
169
+ return data
170
+ end
171
+
172
+ if local_bucketing_initialized?
173
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
174
+ variable_json = bucketed_config.variables[key]
175
+ if variable_json == nil
176
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
177
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
178
+
179
+ return Variable.new({ key: key, value: default, isDefaulted: true })
180
+ end
181
+
182
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_evaluated], target: key })
183
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
184
+
185
+ Variable.new({
186
+ key: key,
187
+ type: variable_json['type'],
188
+ value: variable_json['value'],
189
+ isDefaulted: false
190
+ })
191
+ else
192
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
193
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
194
+
195
+ Variable.new({ key: key, value: default, isDefaulted: true })
196
+ end
115
197
  end
116
198
 
117
199
  # Get variable by key for user data
@@ -145,14 +227,14 @@ module DevCycle
145
227
  # HTTP header 'Content-Type'
146
228
  content_type = @api_client.select_header_content_type(['application/json'])
147
229
  if !content_type.nil?
148
- header_params['Content-Type'] = content_type
230
+ header_params['Content-Type'] = content_type
149
231
  end
150
232
 
151
233
  # form parameters
152
234
  form_params = opts[:form_params] || {}
153
235
 
154
236
  # http body (model)
155
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
237
+ post_body = opts[:debug_body] || user_data.to_json
156
238
 
157
239
  # return_type
158
240
  return_type = opts[:debug_return_type] || 'Variable'
@@ -196,8 +278,17 @@ module DevCycle
196
278
 
197
279
  validate_model(user_data)
198
280
 
199
- data, _status_code, _headers = all_variables_with_http_info(user_data, opts)
200
- data
281
+ if @dvc_options.enable_cloud_bucketing
282
+ data, _status_code, _headers = all_variables_with_http_info(user_data, opts)
283
+ return data
284
+ end
285
+
286
+ if local_bucketing_initialized?
287
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
288
+ bucketed_config.variables
289
+ else
290
+ {}
291
+ end
201
292
  end
202
293
 
203
294
  # Get all variables by key for user data
@@ -225,14 +316,14 @@ module DevCycle
225
316
  # HTTP header 'Content-Type'
226
317
  content_type = @api_client.select_header_content_type(['application/json'])
227
318
  if !content_type.nil?
228
- header_params['Content-Type'] = content_type
319
+ header_params['Content-Type'] = content_type
229
320
  end
230
321
 
231
322
  # form parameters
232
323
  form_params = opts[:form_params] || {}
233
324
 
234
325
  # http body (model)
235
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
326
+ post_body = opts[:debug_body] || user_data.to_json
236
327
 
237
328
  # return_type
238
329
  return_type = opts[:debug_return_type] || 'Hash<String, Variable>'
@@ -275,8 +366,16 @@ module DevCycle
275
366
 
276
367
  validate_model(event_data)
277
368
 
278
- data, _status_code, _headers = track_with_http_info(user_data, event_data, opts)
279
- data
369
+ if @dvc_options.enable_cloud_bucketing
370
+ track_with_http_info(user_data, event_data, opts)
371
+ return
372
+ end
373
+
374
+ if local_bucketing_initialized?
375
+ @event_queue.queue_event(user_data, event_data)
376
+ else
377
+ @logger.warn('track called before DVCClient initialized, event will not be tracked')
378
+ end
280
379
  end
281
380
 
282
381
  # Post events to DevCycle for user
@@ -294,9 +393,9 @@ module DevCycle
294
393
  end
295
394
 
296
395
  user_data_and_events_body = DevCycle::UserDataAndEventsBody.new({
297
- user: user_data,
298
- events: [event_data]
299
- })
396
+ user: user_data,
397
+ events: [event_data]
398
+ })
300
399
 
301
400
  # resource path
302
401
  local_var_path = '/v1/track'
@@ -311,7 +410,7 @@ module DevCycle
311
410
  # HTTP header 'Content-Type'
312
411
  content_type = @api_client.select_header_content_type(['application/json'])
313
412
  if !content_type.nil?
314
- header_params['Content-Type'] = content_type
413
+ header_params['Content-Type'] = content_type
315
414
  end
316
415
 
317
416
  # form parameters
@@ -345,5 +444,13 @@ module DevCycle
345
444
  end
346
445
  return data, status_code, headers
347
446
  end
447
+
448
+ def flush_events
449
+ @event_queue.flush_events
450
+ end
451
+
452
+ def local_bucketing_initialized?
453
+ !@localbucketing.nil? && @localbucketing.initialized
454
+ end
348
455
  end
349
456
  end
@@ -31,7 +31,7 @@ module DevCycle
31
31
  # @option config [Configuration] Configuration for initializing the object, default to Configuration.default
32
32
  def initialize(config = Configuration.default)
33
33
  @config = config
34
- @user_agent = "OpenAPI-Generator/#{VERSION}/ruby"
34
+ @user_agent = "DevCycle-Ruby:#{VERSION}"
35
35
  @default_headers = {
36
36
  'Content-Type' => 'application/json',
37
37
  'User-Agent' => @user_agent
@@ -137,9 +137,9 @@ module DevCycle
137
137
 
138
138
  attr_accessor :force_ending_format
139
139
 
140
- # Define if EdgeDB is Enabled (Boolean)
141
- # Default to false
142
- attr_accessor :enable_edge_db
140
+ # Define if EdgeDB is Enabled (Boolean)
141
+ # Default to false
142
+ attr_accessor :enable_edge_db
143
143
 
144
144
  def initialize
145
145
  @scheme = 'https'
@@ -169,7 +169,7 @@ module DevCycle
169
169
 
170
170
  # The default Configuration object.
171
171
  def self.default
172
- @@default ||= Configuration.new
172
+ @default ||= Configuration.new
173
173
  end
174
174
 
175
175
  def configure
@@ -280,7 +280,7 @@ module DevCycle
280
280
  end
281
281
 
282
282
  def enable_edge_db=(enable_edge_db = false)
283
- if(enable_edge_db)
283
+ if (enable_edge_db)
284
284
  @enable_edge_db = true
285
285
  end
286
286
  end
@@ -0,0 +1,21 @@
1
+ module DevCycle
2
+ class BucketedUserConfig
3
+ attr_accessor :project
4
+ attr_accessor :environment
5
+ attr_accessor :features
6
+ attr_accessor :feature_variation_map
7
+ attr_accessor :variable_variation_map
8
+ attr_accessor :variables
9
+ attr_accessor :known_variable_keys
10
+
11
+ def initialize(project, environment, features, feature_var_map, variable_var_map, variables, known_variable_keys)
12
+ @project = project
13
+ @environment = environment
14
+ @features = features
15
+ @feature_variation_map = feature_var_map
16
+ @variable_variation_map = variable_var_map
17
+ @variables = variables
18
+ @known_variable_keys = known_variable_keys
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require 'concurrent-ruby'
5
+ require 'typhoeus'
6
+ require 'json'
7
+
8
+ module DevCycle
9
+ class ConfigManager
10
+ extend T::Sig
11
+ sig { params(
12
+ sdkKey: String,
13
+ local_bucketing: LocalBucketing,
14
+ wait_for_init: T::Boolean
15
+ ).void }
16
+ def initialize(sdkKey, local_bucketing, wait_for_init)
17
+ @first_load = true
18
+ @config_version = "v1"
19
+ @local_bucketing = local_bucketing
20
+ @sdkKey = sdkKey
21
+ @config_e_tag = ""
22
+ @logger = local_bucketing.options.logger
23
+
24
+ @config_poller = Concurrent::TimerTask.new(
25
+ {
26
+ execution_interval: @local_bucketing.options.config_polling_interval_ms.fdiv(1000),
27
+ run_now: true
28
+ }) do |task|
29
+ fetch_config(false, task)
30
+ end
31
+
32
+ t = Thread.new { fetch_config(false, nil) }
33
+ t.join if wait_for_init
34
+ end
35
+
36
+ def fetch_config(retrying, task)
37
+ req = Typhoeus::Request.new(
38
+ get_config_url,
39
+ headers: {
40
+ Accept: "application/json",
41
+ })
42
+
43
+ if @config_e_tag != ""
44
+ req.options[:headers]['If-None-Match'] = @config_e_tag
45
+ end
46
+
47
+ resp = req.run
48
+
49
+ case resp.code
50
+ when 304
51
+ return nil
52
+ when 200
53
+ return set_config(resp.body, resp.headers['Etag'])
54
+ when 403
55
+ raise("Failed to download DevCycle config; Invalid SDK Key.")
56
+ when 500...599
57
+ if !retrying
58
+ return fetch_config(true, task)
59
+ end
60
+ @logger.warn("Failed to download DevCycle config. Status: #{resp.code}")
61
+ else
62
+ if task != nil
63
+ task.shutdown
64
+ end
65
+ raise("Unexpected response code - DevCycle Response: #{Oj.dump(resp)}")
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ def set_config(config, etag)
72
+ if !JSON.parse(config).is_a?(Hash)
73
+ raise("Invalid JSON body parsed from Config Response")
74
+ end
75
+
76
+ @local_bucketing.store_config(@sdkKey, config)
77
+ @config_e_tag = etag
78
+
79
+ if @first_load
80
+ @logger.info("Config Set. Client Initialized.")
81
+ @first_load = false
82
+ @local_bucketing.initialized = true
83
+ @config_poller.execute
84
+ end
85
+ end
86
+
87
+ def get_config_url
88
+ configBasePath = @local_bucketing.options.config_cdn_uri
89
+ "#{configBasePath}/config/#{@config_version}/server/#{@sdkKey}.json"
90
+ end
91
+
92
+ def close
93
+ @config_poller.shutdown
94
+ nil
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,119 @@
1
+ require 'oj'
2
+
3
+ module DevCycle
4
+ class DVCOptions
5
+ attr_reader :config_polling_interval_ms
6
+ attr_reader :enable_edge_db
7
+ attr_reader :enable_cloud_bucketing
8
+ attr_reader :config_cdn_uri
9
+ attr_reader :events_api_uri
10
+ attr_reader :bucketing_api_uri
11
+ attr_reader :logger
12
+
13
+ def initialize(
14
+ enable_cloud_bucketing: false,
15
+ event_flush_interval_ms: 10 * 1000,
16
+ disable_custom_event_logging: false,
17
+ disable_automatic_event_logging: false,
18
+ config_polling_interval_ms: 10000,
19
+ request_timeout_ms: 5000,
20
+ max_event_queue_size: 2000,
21
+ flush_event_queue_size: 1000,
22
+ event_request_chunk_size: 100,
23
+ logger: nil,
24
+ events_api_uri: 'https://events.devcycle.com',
25
+ enable_edge_db: false
26
+ )
27
+ @logger = logger || defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
28
+ @enable_cloud_bucketing = enable_cloud_bucketing
29
+
30
+ @enable_edge_db = enable_edge_db
31
+
32
+ if !@enable_cloud_bucketing && @enable_edge_db
33
+ raise ArgumentError.new('Cannot enable edgedb without enabling cloud bucketing.')
34
+ end
35
+
36
+ if config_polling_interval_ms < 1000
37
+ @logger.warn('config_polling_interval cannot be less than 1000ms, defaulting to 10000ms')
38
+ config_polling_interval_ms = 10000
39
+ end
40
+ @config_polling_interval_ms = config_polling_interval_ms
41
+
42
+ if request_timeout_ms <= 5000
43
+ request_timeout_ms = 5000
44
+ end
45
+ @request_timeout_ms = request_timeout_ms
46
+
47
+
48
+ if event_flush_interval_ms < 500 || event_flush_interval_ms > (60 * 1000)
49
+ raise ArgumentError.new('event_flush_interval_ms must be between 500ms and 1 minute')
50
+ end
51
+ @event_flush_interval_ms = event_flush_interval_ms
52
+
53
+ if flush_event_queue_size >= max_event_queue_size
54
+ raise ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} must be " +
55
+ "smaller than max_event_queue_size: #{@max_event_queue_size}")
56
+ elsif flush_event_queue_size < event_request_chunk_size || max_event_queue_size < event_request_chunk_size
57
+ throw ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} and " +
58
+ "max_event_queue_size: #{max_event_queue_size} " +
59
+ "must be larger than event_request_chunk_size: #{event_request_chunk_size}")
60
+ elsif flush_event_queue_size > 20000 || max_event_queue_size > 20000
61
+ raise ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} or " +
62
+ "max_event_queue_size: #{max_event_queue_size} must be smaller than 20,000")
63
+ end
64
+ @flush_event_queue_size = flush_event_queue_size
65
+ @max_event_queue_size = max_event_queue_size
66
+ @event_request_chunk_size = event_request_chunk_size
67
+
68
+ @disable_custom_event_logging = disable_custom_event_logging
69
+ @disable_automatic_event_logging = disable_automatic_event_logging
70
+ @config_cdn_uri = "https://config-cdn.devcycle.com"
71
+ @events_api_uri = events_api_uri
72
+ @bucketing_api_uri = "https://bucketing-api.devcyle.com"
73
+ end
74
+
75
+ def event_queue_options
76
+ EventQueueOptions.new(
77
+ @event_flush_interval_ms,
78
+ @disable_automatic_event_logging,
79
+ @disable_custom_event_logging,
80
+ @max_event_queue_size,
81
+ @flush_event_queue_size,
82
+ @events_api_uri,
83
+ @event_request_chunk_size,
84
+ @logger
85
+ )
86
+ end
87
+ end
88
+
89
+ class EventQueueOptions
90
+ attr_reader :event_flush_interval_ms
91
+ attr_reader :disable_automatic_event_logging
92
+ attr_reader :disable_custom_event_logging
93
+ attr_reader :max_event_queue_size
94
+ attr_reader :flush_event_queue_size
95
+ attr_reader :events_api_uri
96
+ attr_reader :event_request_chunk_size
97
+ attr_reader :logger
98
+
99
+ def initialize (
100
+ event_flush_interval_ms,
101
+ disable_automatic_event_logging,
102
+ disable_custom_event_logging,
103
+ max_event_queue_size,
104
+ flush_event_queue_size,
105
+ events_api_uri,
106
+ event_request_chunk_size,
107
+ logger
108
+ )
109
+ @event_flush_interval_ms = event_flush_interval_ms
110
+ @disable_automatic_event_logging = disable_automatic_event_logging
111
+ @disable_custom_event_logging = disable_custom_event_logging
112
+ @max_event_queue_size = max_event_queue_size
113
+ @flush_event_queue_size = flush_event_queue_size
114
+ @events_api_uri = events_api_uri
115
+ @event_request_chunk_size = event_request_chunk_size
116
+ @logger = logger
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,120 @@
1
+ require 'typhoeus'
2
+ require 'sorbet-runtime'
3
+ require 'concurrent-ruby'
4
+
5
+ module DevCycle
6
+ class EventQueue
7
+ extend T::Sig
8
+
9
+ sig { params(sdkKey: String, options: EventQueueOptions, local_bucketing: LocalBucketing).void }
10
+ def initialize(sdkKey, options, local_bucketing)
11
+ @sdkKey = sdkKey
12
+ @events_api_uri = options.events_api_uri
13
+ @logger = options.logger
14
+ @event_flush_interval_ms = options.event_flush_interval_ms
15
+ @flush_event_queue_size = options.flush_event_queue_size
16
+ @max_event_queue_size = options.max_event_queue_size
17
+
18
+ @flush_timer_task = Concurrent::TimerTask.new(
19
+ execution_interval: @event_flush_interval_ms.fdiv(1000),
20
+ run_now: true
21
+ ) {
22
+ flush_events
23
+ }
24
+ @flush_timer_task.execute
25
+ @flush_timer_task.add_observer(FlushTimerTaskObserver.new)
26
+
27
+ @local_bucketing = local_bucketing
28
+ @local_bucketing.init_event_queue(options)
29
+
30
+ @flush_mutex = Mutex.new
31
+ nil
32
+ end
33
+
34
+ def close
35
+ @flush_timer_task.shutdown
36
+ flush_events
37
+ end
38
+
39
+ sig { returns(NilClass) }
40
+ def flush_events
41
+ @flush_mutex.synchronize do
42
+ payloads = @local_bucketing.flush_event_queue
43
+ if payloads.length == 0
44
+ return
45
+ end
46
+ eventCount = payloads.reduce(0) { |sum, payload| sum + payload.eventCount }
47
+ @logger.debug("DVC: Flushing #{eventCount} event(s) for #{payloads.length} user(s)")
48
+
49
+ payloads.each do |payload|
50
+ begin
51
+ response = Typhoeus.post(
52
+ @events_api_uri + '/v1/events/batch',
53
+ headers: { 'Authorization': @sdkKey },
54
+ body: { 'batch': payload.records }.to_json
55
+ )
56
+ if response.code != 201
57
+ @logger.error("Error publishing events, status: #{response.code}, body: #{response.return_message}")
58
+ @local_bucketing.on_payload_failure(payload.payloadId, response.code >= 500)
59
+ else
60
+ @logger.debug("DVC: Flushed #{eventCount} event(s), for #{payload.records.length} user(s)")
61
+ @local_bucketing.on_payload_success(payload.payloadId)
62
+ end
63
+ rescue => e
64
+ @logger.error("DVC Error Flushing Events response message: #{e.message}")
65
+ @local_bucketing.on_payload_failure(payload.payloadId, false)
66
+ end
67
+ end
68
+ end
69
+ nil
70
+ end
71
+
72
+ # Todo: implement PopulatedUser
73
+ sig { params(user: UserData, event: Event).returns(NilClass) }
74
+ def queue_event(user, event)
75
+ if max_event_queue_size_reached?
76
+ @logger.warn("Max event queue size reached, dropping event: #{event}")
77
+ return
78
+ end
79
+
80
+ @local_bucketing.queue_event(user, event)
81
+ nil
82
+ end
83
+
84
+ sig { params(event: Event, bucketed_config: T.nilable(BucketedUserConfig)).returns(NilClass) }
85
+ def queue_aggregate_event(event, bucketed_config)
86
+ if max_event_queue_size_reached?
87
+ @logger.warn("Max event queue size reached, dropping event: #{event}")
88
+ return
89
+ end
90
+
91
+ @local_bucketing.queue_aggregate_event(event, bucketed_config)
92
+ nil
93
+ end
94
+
95
+ sig { returns(T::Boolean) }
96
+ def max_event_queue_size_reached?
97
+ queue_size = @local_bucketing.check_event_queue_size()
98
+ if queue_size >= @flush_event_queue_size
99
+ flush_events
100
+ if queue_size >= @max_event_queue_size
101
+ return true
102
+ end
103
+ end
104
+ false
105
+ end
106
+ end
107
+
108
+ # Todo: remove when done testing
109
+ class FlushTimerTaskObserver
110
+ def update(time, result, ex)
111
+ return if ex.nil?
112
+
113
+ if ex.is_a?(Concurrent::TimeoutError)
114
+ print("DVC FlushTimerTaskObserver: Execution timed out")
115
+ else
116
+ print("DVC FlushTimerTaskObserver: Execution failed with error: #{ex}")
117
+ end
118
+ end
119
+ end
120
+ end