rox-rollout 4.1.0 → 5.0.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.
Files changed (121) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +51 -23
  3. data/.editorconfig +12 -0
  4. data/.github/workflows/ruby.yml +20 -0
  5. data/.gitignore +2 -0
  6. data/.rubocop.yml +17 -0
  7. data/Gemfile +2 -2
  8. data/README.md +32 -0
  9. data/Rakefile +12 -9
  10. data/bin/console +3 -4
  11. data/e2e-server/run_server.sh +12 -0
  12. data/e2e-server/server.rb +158 -0
  13. data/e2e/container.rb +11 -14
  14. data/e2e/custom_props.rb +9 -9
  15. data/e2e/rox_e2e_test.rb +18 -20
  16. data/e2e/test_vars.rb +3 -6
  17. data/example/local.rb +40 -0
  18. data/lib/rox/core/analytics.rb +18 -0
  19. data/lib/rox/core/client/buid.rb +9 -44
  20. data/lib/rox/core/client/device_properties.rb +8 -2
  21. data/lib/rox/core/client/dynamic_api.rb +53 -15
  22. data/lib/rox/core/client/internal_flags.rb +14 -2
  23. data/lib/rox/core/client/sdk_settings.rb +1 -1
  24. data/lib/rox/core/configuration/configuration.rb +1 -1
  25. data/lib/rox/core/configuration/configuration_fetched_args.rb +2 -2
  26. data/lib/rox/core/configuration/configuration_fetched_invoker.rb +8 -3
  27. data/lib/rox/core/configuration/configuration_parser.rb +39 -38
  28. data/lib/rox/core/configuration/fetcher_error.rb +1 -1
  29. data/lib/rox/core/configuration/fetcher_status.rb +1 -1
  30. data/lib/rox/core/configuration/models/experiment_model.rb +1 -1
  31. data/lib/rox/core/configuration/models/target_group_model.rb +1 -1
  32. data/lib/rox/core/consts/build.rb +3 -3
  33. data/lib/rox/core/consts/environment.rb +34 -10
  34. data/lib/rox/core/consts/property_type.rb +18 -17
  35. data/lib/rox/core/context/merged_context.rb +2 -1
  36. data/lib/rox/core/core.rb +101 -33
  37. data/lib/rox/core/entities/default_flag_values.rb +10 -0
  38. data/lib/rox/core/entities/flag.rb +22 -7
  39. data/lib/rox/core/entities/flag_setter.rb +6 -6
  40. data/lib/rox/core/entities/rox_double.rb +11 -0
  41. data/lib/rox/core/entities/rox_int.rb +11 -0
  42. data/lib/rox/core/entities/{variant.rb → rox_string.rb} +20 -13
  43. data/lib/rox/core/error_handling/exception_trigger.rb +10 -0
  44. data/lib/rox/core/error_handling/userspace_handler_exception.rb +12 -0
  45. data/lib/rox/core/error_handling/userspace_unhandled_error_invoker.rb +41 -0
  46. data/lib/rox/core/impression/impression_args.rb +2 -2
  47. data/lib/rox/core/impression/impression_invoker.rb +41 -7
  48. data/lib/rox/core/impression/models/experiment.rb +1 -1
  49. data/lib/rox/core/impression/models/reporting_value.rb +10 -2
  50. data/lib/rox/core/logging/logging.rb +3 -3
  51. data/lib/rox/core/logging/no_op_logger.rb +1 -1
  52. data/lib/rox/core/network/configuration_fetch_result.rb +1 -1
  53. data/lib/rox/core/network/configuration_fetcher.rb +9 -8
  54. data/lib/rox/core/network/configuration_fetcher_base.rb +1 -1
  55. data/lib/rox/core/network/configuration_fetcher_roxy.rb +4 -4
  56. data/lib/rox/core/network/configuration_fetcher_self_managed.rb +30 -0
  57. data/lib/rox/core/network/configuration_source.rb +1 -1
  58. data/lib/rox/core/network/request.rb +3 -3
  59. data/lib/rox/core/network/request_configuration_builder.rb +12 -9
  60. data/lib/rox/core/network/request_data.rb +1 -1
  61. data/lib/rox/core/network/response.rb +20 -4
  62. data/lib/rox/core/network/state_sender.rb +146 -0
  63. data/lib/rox/core/notifications/notification_listener.rb +4 -4
  64. data/lib/rox/core/properties/custom_property.rb +1 -1
  65. data/lib/rox/core/properties/custom_property_type.rb +1 -1
  66. data/lib/rox/core/properties/device_property.rb +2 -2
  67. data/lib/rox/core/properties/property_factory.rb +132 -0
  68. data/lib/rox/core/register/registerer.rb +13 -12
  69. data/lib/rox/core/reporting/error_reporter.rb +14 -13
  70. data/lib/rox/core/repositories/custom_property_repository.rb +1 -1
  71. data/lib/rox/core/repositories/experiment_repository.rb +2 -4
  72. data/lib/rox/core/repositories/flag_repository.rb +8 -8
  73. data/lib/rox/core/repositories/roxx/experiments_extensions.rb +8 -8
  74. data/lib/rox/core/repositories/roxx/properties_extensions.rb +32 -7
  75. data/lib/rox/core/repositories/target_group_repository.rb +2 -4
  76. data/lib/rox/core/roxx/evaluation_result.rb +2 -0
  77. data/lib/rox/core/roxx/node.rb +1 -1
  78. data/lib/rox/core/roxx/parser.rb +46 -20
  79. data/lib/rox/core/roxx/regular_expression_extensions.rb +6 -3
  80. data/lib/rox/core/roxx/string_tokenizer.rb +3 -1
  81. data/lib/rox/core/roxx/symbols.rb +1 -1
  82. data/lib/rox/core/roxx/token_type.rb +1 -1
  83. data/lib/rox/core/roxx/tokenized_expression.rb +16 -12
  84. data/lib/rox/core/roxx/value_compare_extensions.rb +49 -29
  85. data/lib/rox/core/security/signature_verifier.rb +3 -3
  86. data/lib/rox/core/security/signature_verifier_mock.rb +12 -0
  87. data/lib/rox/core/utils/debouncer.rb +18 -0
  88. data/lib/rox/core/utils/type_utils.rb +1 -1
  89. data/lib/rox/server/client/sdk_settings.rb +1 -1
  90. data/lib/rox/server/client/server_properties.rb +1 -1
  91. data/lib/rox/server/flags/normalize_flag_type.rb +25 -0
  92. data/lib/rox/server/flags/rox_double.rb +8 -0
  93. data/lib/rox/server/flags/rox_flag.rb +1 -1
  94. data/lib/rox/server/flags/rox_int.rb +8 -0
  95. data/lib/rox/server/flags/rox_string.rb +8 -0
  96. data/lib/rox/server/flags/server_entities_provider.rb +14 -4
  97. data/lib/rox/server/logging/server_logger.rb +2 -2
  98. data/lib/rox/server/rox_options.rb +39 -8
  99. data/lib/rox/server/rox_server.rb +81 -60
  100. data/lib/rox/version.rb +1 -1
  101. data/rox.gemspec +13 -9
  102. metadata +95 -36
  103. data/README_DEVELOP.md +0 -19
  104. data/_archive/.document +0 -5
  105. data/_archive/.rspec +0 -1
  106. data/_archive/Gemfile +0 -15
  107. data/_archive/Gemfile.lock +0 -87
  108. data/_archive/README.md +0 -32
  109. data/_archive/README.rdoc +0 -19
  110. data/_archive/Rakefile +0 -50
  111. data/_archive/lib/expr_function_definition.rb +0 -52
  112. data/_archive/lib/function_definition.rb +0 -48
  113. data/_archive/lib/function_token.rb +0 -12
  114. data/_archive/lib/object_extends.rb +0 -12
  115. data/_archive/lib/ruby_interpreter.rb +0 -292
  116. data/_archive/lib/stack.rb +0 -48
  117. data/_archive/lib/string_extends.rb +0 -14
  118. data/_archive/spec/ruby_interpreter_spec.rb +0 -203
  119. data/_archive/spec/spec_helper.rb +0 -30
  120. data/_archive/spec/stack_spec.rb +0 -77
  121. data/lib/rox/server/flags/rox_variant.rb +0 -8
@@ -0,0 +1,10 @@
1
+ module Rox
2
+ module Core
3
+ class DefaultFlagValues
4
+ STRING = ''
5
+ INT = 0
6
+ FLOAT = 0.0
7
+ BOOLEAN = false
8
+ end
9
+ end
10
+ end
@@ -1,20 +1,35 @@
1
- require 'rox/core/entities/variant'
1
+ require 'rox/core/entities/rox_string'
2
+ require 'rox/core/entities/default_flag_values'
2
3
 
3
4
  module Rox
4
5
  module Core
5
- class Flag < Variant
6
+ class Flag < RoxString
6
7
  FLAG_TRUE_VALUE = 'true'.freeze
7
8
  FLAG_FALSE_VALUE = 'false'.freeze
8
9
 
9
- def initialize(default_value = false)
10
- super(default_value ? Flag::FLAG_TRUE_VALUE : Flag::FLAG_FALSE_VALUE, [Flag::FLAG_FALSE_VALUE, Flag::FLAG_TRUE_VALUE])
10
+ def initialize(default_value = DefaultFlagValues::BOOLEAN)
11
+ super(default_value ? Flag::FLAG_TRUE_VALUE : Flag::FLAG_FALSE_VALUE, [Flag::FLAG_FALSE_VALUE,
12
+ Flag::FLAG_TRUE_VALUE])
11
13
  end
12
14
 
13
15
  def enabled?(context)
14
- value(context) == Flag::FLAG_TRUE_VALUE
16
+ merged_context = MergedContext.new(@parser&.global_context, context)
17
+ value = internal_enabled?(merged_context)
18
+ if [true, false].include? value
19
+ send_impressions(value, merged_context)
20
+ return value
21
+ end
22
+
23
+ send_impressions(DefaultFlagValues::BOOLEAN, merged_context)
24
+ DefaultFlagValues.BOOLEAN
25
+ end
26
+
27
+ def value(context = nil)
28
+ merged_context = MergedContext.new(@parser&.global_context, context)
29
+ internal_value(merged_context, false)
15
30
  end
16
31
 
17
- def internal_enabled?(context, nil_instead_of_default)
32
+ def internal_enabled?(context, nil_instead_of_default = false)
18
33
  val = internal_value(context, nil_instead_of_default)
19
34
  nil_instead_of_default && val.nil? ? nil : (val == Flag::FLAG_TRUE_VALUE)
20
35
  end
@@ -28,4 +43,4 @@ module Rox
28
43
  end
29
44
  end
30
45
  end
31
- end
46
+ end
@@ -7,9 +7,9 @@ module Rox
7
7
  @experiment_repository = experiment_repository
8
8
  @impression_invoker = impression_invoker
9
9
 
10
- @flag_repository.register_flag_added_handler do |variant|
11
- exp = @experiment_repository.experiment_by_flag(variant.name)
12
- set_flag_data(variant, exp)
10
+ @flag_repository.register_flag_added_handler do |string|
11
+ exp = @experiment_repository.experiment_by_flag(string.name)
12
+ set_flag_data(string, exp)
13
13
  end
14
14
  end
15
15
 
@@ -31,9 +31,9 @@ module Rox
31
31
  end
32
32
  end
33
33
 
34
- def set_flag_data(variant, experiment = nil)
35
- variant.set_for_evaluation(@parser, experiment, @impression_invoker)
34
+ def set_flag_data(string, experiment = nil)
35
+ string.set_for_evaluation(@parser, experiment, @impression_invoker)
36
36
  end
37
37
  end
38
38
  end
39
- end
39
+ end
@@ -0,0 +1,11 @@
1
+ require 'rox/core/entities/rox_string'
2
+
3
+ module Rox
4
+ module Core
5
+ class RoxDouble < RoxString
6
+ def value(context = nil)
7
+ internal_value(context, false, Float)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'rox/core/entities/rox_string'
2
+
3
+ module Rox
4
+ module Core
5
+ class RoxInt < RoxString
6
+ def value(context = nil)
7
+ internal_value(context, false, Integer)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,22 +4,31 @@ require 'rox/core/impression/models/reporting_value'
4
4
 
5
5
  module Rox
6
6
  module Core
7
- class Variant
8
- attr_accessor :default_value, :options, :name, :context, :condition, :parser, :impression_invoker, :client_experiment
7
+ class RoxString
8
+ attr_accessor :default_value, :options, :name, :condition, :parser, :impression_invoker,
9
+ :client_experiment
9
10
 
10
11
  def initialize(default_value, options = [])
11
12
  @default_value = default_value
12
13
  @options = options.clone
14
+ raise ArgumentError, 'options should be an array' unless options.is_a?(Array)
15
+ if options.length > 0 && options.any? { |x| x.class != default_value.class }
16
+ raise ArgumentError, 'options should be the same type as default value'
17
+ end
13
18
  @options << default_value unless options.include?(default_value)
14
19
 
15
20
  @condition = nil
16
21
  @parser = nil
17
- @context = nil
18
22
  @impression_invoker = nil
19
23
  @client_experiment = nil
20
24
  @name = nil
21
25
  end
22
26
 
27
+ def send_impressions(return_value, merged_context)
28
+ reporting_value = ReportingValue.new(@name, return_value, @client_experiment)
29
+ @impression_invoker&.invoke(reporting_value, @client_experiment, merged_context)
30
+ end
31
+
23
32
  def set_for_evaluation(parser, experiment, impression_invoker)
24
33
  if experiment.nil?
25
34
  @client_experiment = nil
@@ -37,24 +46,22 @@ module Rox
37
46
  internal_value(context, false)
38
47
  end
39
48
 
40
- def internal_value(context, nil_instead_of_default)
49
+ def internal_value(context, nil_instead_of_default, evaluated_type = String)
41
50
  return_value = nil_instead_of_default ? nil : @default_value
42
- merged_context = MergedContext.new(@context, context)
51
+ merged_context = MergedContext.new(@parser&.global_context, context)
43
52
 
44
- if !@parser.nil? && !@condition.nil? && !@condition.empty?
53
+ unless @parser.nil? || @condition.nil? || @condition.empty?
45
54
  evaluation_result = @parser.evaluate_expression(@condition, merged_context)
46
55
  unless evaluation_result.nil?
47
- value = evaluation_result.string_value
48
- if !value.nil? && !value.empty?
49
- return_value = value
50
- end
56
+ value = evaluated_type == String ? evaluation_result.string_value : evaluation_result.value
57
+ is_empty = value.is_a?(String) && value.empty?
58
+ return_value = value if !value.nil? && !is_empty
51
59
  end
52
60
  end
53
61
 
54
- @impression_invoker.invoke(ReportingValue.new(@name, return_value), @client_experiment, merged_context) if @impression_invoker != nil
55
-
62
+ send_impressions(return_value, merged_context)
56
63
  return_value
57
64
  end
58
65
  end
59
66
  end
60
- end
67
+ end
@@ -0,0 +1,10 @@
1
+ module Rox
2
+ module Core
3
+ module ExceptionTrigger
4
+ DYNAMIC_PROPERTIES_RULE = 1
5
+ CONFIGURATION_FETCHED_HANDLER = 2
6
+ IMPRESSION_HANDLER = 3
7
+ CUSTOM_PROPERTY_GENERATOR = 4
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Rox
2
+ module Core
3
+ class UserspaceHandlerException < StandardError
4
+ def initialize(exception_source, exception_trigger, exception)
5
+ @exception_source = exception_source
6
+ @exception_trigger = exception_trigger
7
+ @exception = exception
8
+ super('user unhandled exception in roxx expression')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require 'rox/core/logging/logging'
2
+
3
+ module Rox
4
+ module Core
5
+ class UserspaceUnhandledErrorInvoker
6
+ def initialize(user_unhandler_error_handler = nil)
7
+ @user_unhandler_error_handler = user_unhandler_error_handler
8
+ end
9
+
10
+ def invoke(exception_source, exception_trigger, exception)
11
+ unless @user_unhandler_error_handler
12
+ Logging.logger.error("User Unhandled Error Occurred, no fallback handler was set, exception ignored: #{exception}")
13
+ return
14
+ end
15
+
16
+ begin
17
+ userspace_unhandled_error_args = UserspaceUnhandledErrorArgs.new(exception_source, exception_trigger, exception)
18
+ @user_unhandler_error_handler.call(userspace_unhandled_error_args)
19
+ rescue StandardError => e
20
+ Logging.logger.error("User Unhandled Error Handler itself threw an exception. original exception: #{e}")
21
+ end
22
+ end
23
+
24
+ def handler=(handler)
25
+ @user_unhandler_error_handler = handler
26
+ end
27
+ end
28
+
29
+ class UserspaceUnhandledErrorArgs
30
+ def initialize(exception_source = nil, exception_trigger = nil, exception = nil)
31
+ @exception_source = exception_source
32
+ @exception_trigger = exception_trigger
33
+ @exception = exception
34
+ end
35
+
36
+ def to_s
37
+ "UserspaceUnhandledErrorArgs(#{@exception_source}, #{@exception_trigger}, #{@exception})"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  module Rox
2
2
  module Core
3
- ImpressionArgs = Struct.new(:reporting_value, :experiment, :context)
3
+ ImpressionArgs = Struct.new(:reporting_value, :context)
4
4
  end
5
- end
5
+ end
@@ -1,23 +1,54 @@
1
1
  require 'rox/core/impression/impression_args'
2
+ require 'rox/core/logging/logging'
3
+ require 'rox/core/error_handling/exception_trigger'
4
+ require 'rox/core/consts/property_type'
2
5
 
3
6
  module Rox
4
7
  module Core
5
8
  class ImpressionInvoker
6
- def initialize(internal_flags, custom_property_repository, device_properties, analytics_client, is_roxy)
9
+ def initialize(internal_flags, custom_property_repository, device_properties, analytics_client, is_roxy, user_unhandled_error_invoker)
7
10
  @internal_flags = internal_flags
8
11
  @custom_property_repository = custom_property_repository
9
12
  @device_properties = device_properties
10
13
  @analytics_client = analytics_client
11
14
  @is_roxy = is_roxy
15
+ @user_unhandled_error_invoker = user_unhandled_error_invoker
12
16
 
13
17
  @impression_handlers = []
14
18
  @mutex = Mutex.new
15
19
  end
16
20
 
17
- def invoke(reporting_value, client_experiment, context)
18
- # TODO: Implement analytics logic
21
+ def invoke(reporting_value, stickiness_property, context)
22
+ begin
23
+ analytics_enabled = @internal_flags.enabled?('rox.internal.analytics')
24
+ if analytics_enabled && !@is_roxy
25
+ prop = @custom_property_repository.custom_property(stickiness_property) || @custom_property_repository.custom_property("rox.#{Rox::Core::PropertyType::DISTINCT_ID.name}")
26
+ distinct_id = '(null_distinct_id'
27
+ unless prop.nil?
28
+ prop_value = prop.value(context)
29
+ distinct_id = prop_value if prop_value.instance_of?(String)
30
+ end
19
31
 
20
- raise_impression_event(ImpressionArgs.new(reporting_value, client_experiment, context))
32
+ event_time = (Time.now.to_f * 1000.0).to_i
33
+ begin
34
+ event_time = ENV['rox.analytics.ms'].to_i
35
+ rescue StandardError
36
+ end
37
+
38
+ @analytics_client.track({
39
+ flag: reporting_value.name,
40
+ value: reporting_value.value,
41
+ distinctId: distinct_id,
42
+ experimentVersion: '0',
43
+ type: 'IMPRESSION',
44
+ time: event_time
45
+ })
46
+ end
47
+ rescue StandardError => ex
48
+ Logging.logger.error('Failed to send analytics', ex)
49
+ end
50
+
51
+ raise_impression_event(ImpressionArgs.new(reporting_value, context))
21
52
  end
22
53
 
23
54
  def register_impression_handler(&block)
@@ -32,10 +63,13 @@ module Rox
32
63
  handlers = @impression_handlers.clone
33
64
  end
34
65
 
35
- handlers.each do |handler|
36
- handler.call(args)
66
+ begin
67
+ handlers.each { |handler| handler.call(args) }
68
+ rescue StandardError => e
69
+ user_unhandled_error_invoker.invoke(handler, ExceptionTrigger::IMPRESSION_HANDLER, e)
70
+ Logging.logger.error('Impresssion handler exception', ex)
37
71
  end
38
72
  end
39
73
  end
40
74
  end
41
- end
75
+ end
@@ -11,4 +11,4 @@ module Rox
11
11
  end
12
12
  end
13
13
  end
14
- end
14
+ end
@@ -1,5 +1,13 @@
1
1
  module Rox
2
2
  module Core
3
- ReportingValue = Struct.new(:name, :value)
3
+ class ReportingValue
4
+ attr_reader :name, :value, :targeting
5
+
6
+ def initialize(name, value, experiment = nil)
7
+ @name = name
8
+ @value = value
9
+ @targeting = !experiment.nil?
10
+ end
11
+ end
4
12
  end
5
- end
13
+ end
@@ -5,8 +5,8 @@ module Rox
5
5
  class Logging
6
6
  @logger = nil
7
7
 
8
- def self.logger=(logger)
9
- @logger = logger
8
+ class << self
9
+ attr_writer :logger
10
10
  end
11
11
 
12
12
  def self.logger
@@ -14,4 +14,4 @@ module Rox
14
14
  end
15
15
  end
16
16
  end
17
- end
17
+ end
@@ -8,4 +8,4 @@ module Rox
8
8
  def warn(message, ex = nil); end
9
9
  end
10
10
  end
11
- end
11
+ end
@@ -2,4 +2,4 @@ module Rox
2
2
  module Core
3
3
  ConfigurationFetchResult = Struct.new(:data, :source)
4
4
  end
5
- end
5
+ end
@@ -9,18 +9,18 @@ module Rox
9
9
  source = ConfigurationSource::CDN
10
10
  begin
11
11
  fetch_result = fetch_from_cdn
12
- return ConfigurationFetchResult.new(fetch_result.text, source) if fetch_result.success_status_code?
13
-
14
- if [403, 404].include?(fetch_result.status_code)
12
+ if fetch_result.success?
13
+ return ConfigurationFetchResult.new(fetch_result.text, source)
14
+ else
15
15
  write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_result, false, ConfigurationSource::API)
16
16
  source = ConfigurationSource::API
17
17
  fetch_result = fetch_from_api
18
- return ConfigurationFetchResult.new(fetch_result.text, source) if fetch_result.success_status_code?
18
+ return ConfigurationFetchResult.new(fetch_result.text, source) if fetch_result.success?
19
19
  end
20
20
 
21
21
  write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_result)
22
- rescue StandardError => ex
23
- write_fetch_exception_to_log_and_invoke_fetch_handler(source, ex)
22
+ rescue StandardError => e
23
+ write_fetch_exception_to_log_and_invoke_fetch_handler(source, e)
24
24
  end
25
25
 
26
26
  nil
@@ -31,8 +31,9 @@ module Rox
31
31
  end
32
32
 
33
33
  def fetch_from_api
34
- @request.send_get(@request_configuration_builder.build_for_api)
34
+ api_request = @request_configuration_builder.build_for_api
35
+ @request.send_post(api_request.url, api_request.query_params)
35
36
  end
36
37
  end
37
38
  end
38
- end
39
+ end
@@ -22,4 +22,4 @@ module Rox
22
22
  end
23
23
  end
24
24
  end
25
- end
25
+ end
@@ -9,13 +9,13 @@ module Rox
9
9
  source = ConfigurationSource::ROXY
10
10
  begin
11
11
  fetch_roxy = fetch_from_roxy
12
- if fetch_roxy.success_status_code?
12
+ if fetch_roxy.success?
13
13
  return ConfigurationFetchResult.new(fetch_roxy.text, source)
14
14
  else
15
15
  write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_roxy)
16
16
  end
17
- rescue StandardError => ex
18
- write_fetch_exception_to_log_and_invoke_fetch_handler(source, ex)
17
+ rescue StandardError => e
18
+ write_fetch_exception_to_log_and_invoke_fetch_handler(source, e)
19
19
  end
20
20
 
21
21
  nil
@@ -26,4 +26,4 @@ module Rox
26
26
  end
27
27
  end
28
28
  end
29
- end
29
+ end