Skip to content

Commit 14b676f

Browse files
committed
Fix edge-cases mentioned in review
* Improve performance of the hot-paths * Add error handling in remote configuration * Complete shutdown process
1 parent db96bdf commit 14b676f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+426
-616
lines changed

lib/datadog/core/configuration/components.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ def shutdown!(replacement = nil)
202202
# Shutdown DI after remote, since remote config triggers DI operations.
203203
dynamic_instrumentation&.shutdown!
204204

205+
# Shutdown OpenFeature component
206+
open_feature&.shutdown!
207+
205208
# Decommission AppSec
206209
appsec&.shutdown!
207210

lib/datadog/open_feature/component.rb

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,32 @@ module Datadog
1111
module OpenFeature
1212
# This class is the entry point for the OpenFeature component
1313
class Component
14-
attr_reader :telemetry, :engine
14+
attr_reader :engine
1515

1616
def self.build(settings, agent_settings, logger:, telemetry:)
17-
return unless settings.open_feature.enabled
17+
return unless settings.respond_to?(:open_feature) && settings.open_feature.enabled
1818

19-
unless settings.remote.enabled
20-
logger.warn('OpenFeature could not be enabled as Remote Configuration is currently disabled. To enable Remote Configuration, see https://docs.datadoghq.com/agent/remote_config')
19+
unless settings.respond_to?(:remote) && settings.remote.enabled
20+
message = 'OpenFeature could not be enabled as Remote Configuration is currently disabled. ' \
21+
'To enable Remote Configuration, see https://docs.datadoghq.com/agent/remote_config'
22+
logger.warn(message)
2123

2224
return
2325
end
2426

2527
new(settings, agent_settings, logger: logger, telemetry: telemetry)
26-
rescue
27-
logger.warn('OpenFeature is disabled, see logged errors above')
28-
29-
nil
3028
end
3129

3230
def initialize(settings, agent_settings, logger:, telemetry:)
33-
@settings = settings
34-
@agent_settings = agent_settings
35-
@logger = logger
36-
@telemetry = telemetry
37-
3831
transport = Transport::HTTP.build(agent_settings: agent_settings, logger: logger)
39-
@worker = Exposures::Worker.new(settings: settings, transport: transport, logger: logger)
40-
@reporter = Exposures::Reporter.new(@worker, telemetry: telemetry, logger: logger)
41-
@engine = EvaluationEngine.new(@reporter, telemetry: telemetry, logger: logger)
42-
end
32+
@worker = Exposures::Worker.new(settings: settings, transport: transport, telemetry: telemetry, logger: logger)
4333

44-
def flush
45-
@worker&.flush
34+
reporter = Exposures::Reporter.new(@worker, telemetry: telemetry, logger: logger)
35+
@engine = EvaluationEngine.new(reporter, telemetry: telemetry, logger: logger)
4636
end
4737

4838
def shutdown!
49-
@worker&.graceful_shutdown
39+
@worker.graceful_shutdown
5040
end
5141
end
5242
end

lib/datadog/open_feature/evaluation_engine.rb

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,62 +8,51 @@ module Datadog
88
module OpenFeature
99
# This class performs the evaluation of the feature flag
1010
class EvaluationEngine
11-
attr_accessor :configuration
11+
ReconfigurationError = Class.new(StandardError)
1212

13-
ALLOWED_TYPES = %i[boolean string number float integer object].freeze
13+
ALLOWED_TYPES = %w[boolean string number float integer object].freeze
1414

1515
def initialize(reporter, telemetry:, logger:)
1616
@reporter = reporter
1717
@telemetry = telemetry
1818
@logger = logger
1919

20-
@mutex = Mutex.new
2120
@evaluator = NoopEvaluator.new(nil)
22-
@configuration = nil
2321
end
2422

25-
def fetch_value(flag_key:, expected_type:, evaluation_context: nil)
23+
def fetch_value(flag_key:, default_value:, expected_type:, evaluation_context: nil)
2624
unless ALLOWED_TYPES.include?(expected_type)
2725
message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
28-
29-
return ResolutionDetails.new(
30-
error_code: Ext::UNKNOWN_TYPE,
31-
error_message: message,
32-
reason: Ext::ERROR,
33-
log?: false,
34-
error?: true
26+
return ResolutionDetails.build_error(
27+
value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
3528
)
3629
end
3730

38-
result = @evaluator.get_assignment(flag_key, evaluation_context, expected_type)
31+
context = evaluation_context&.fields || {}
32+
result = @evaluator.get_assignment(flag_key, default_value, context, expected_type)
33+
3934
@reporter.report(result, flag_key: flag_key, context: evaluation_context)
4035

4136
result
4237
rescue => e
4338
@telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
4439

45-
ResolutionDetails.new(
46-
error_code: Ext::PROVIDER_FATAL,
47-
error_message: e.message,
48-
reason: Ext::ERROR,
49-
error?: true,
50-
log?: false
40+
ResolutionDetails.build_error(
41+
value: default_value, error_code: Ext::PROVIDER_FATAL, error_message: e.message
5142
)
5243
end
5344

54-
def reconfigure!
55-
@logger.debug('OpenFeature: Removing configuration') if @configuration.nil?
45+
def reconfigure!(configuration)
46+
@logger.debug('OpenFeature: Removing configuration') if configuration.nil?
5647

57-
@mutex.synchronize do
58-
@evaluator = NoopEvaluator.new(@configuration)
59-
end
48+
@evaluator = NoopEvaluator.new(configuration)
6049
rescue => e
61-
error_message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
50+
message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
6251

63-
@logger.error("#{error_message}, error #{e.inspect}")
64-
@telemetry.report(e, description: error_message)
52+
@logger.error("#{message}, error #{e.inspect}")
53+
@telemetry.report(e, description: message)
6554

66-
raise e
55+
raise ReconfigurationError, e.message
6756
end
6857
end
6958
end

lib/datadog/open_feature/exposures/batch_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def initialize(settings)
1212
def payload_for(events)
1313
{
1414
context: @context,
15-
exposures: events.map(&:to_h)
15+
exposures: events
1616
}
1717
end
1818

lib/datadog/open_feature/exposures/buffer.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ module OpenFeature
77
module Exposures
88
# This class is a buffer for exposure events that evicts at random and
99
# keeps track of the number of dropped events
10+
#
11+
# WARNING: This class does not work as intended on JRuby
1012
class Buffer < Core::Buffer::CRuby
1113
DEFAULT_LIMIT = 1_000
1214

15+
attr_reader :dropped_count
16+
1317
def initialize(limit = DEFAULT_LIMIT)
1418
@dropped = 0
19+
@dropped_count = 0
1520

1621
super
1722
end
@@ -21,10 +26,10 @@ def initialize(limit = DEFAULT_LIMIT)
2126
def drain!
2227
drained = super
2328

24-
dropped = @dropped
29+
@dropped_count = @dropped
2530
@dropped = 0
2631

27-
[drained, dropped]
32+
drained
2833
end
2934

3035
def replace!(item)

lib/datadog/open_feature/exposures/deduplicator.rb

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'zlib'
43
require_relative '../../core/utils/lru_cache'
54

65
module Datadog
@@ -15,25 +14,16 @@ def initialize(limit: DEFAULT_CACHE_LIMIT)
1514
@mutex = Mutex.new
1615
end
1716

18-
def duplicate?(event)
19-
cache_key = digest(event.flag_key, event.targeting_key)
20-
cache_digest = digest(event.allocation_key, event.variation_key)
21-
17+
def duplicate?(key, value)
2218
@mutex.synchronize do
23-
stored = @cache[cache_key]
24-
return true if stored == cache_digest
19+
stored = @cache[key]
20+
return true if stored == value
2521

26-
@cache[cache_key] = cache_digest
22+
@cache[key] = value
2723
end
2824

2925
false
3026
end
31-
32-
private
33-
34-
def digest(left, right)
35-
Zlib.crc32("#{left}:#{right}")
36-
end
3727
end
3828
end
3929
end

lib/datadog/open_feature/exposures/event.rb

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ module Datadog
66
module OpenFeature
77
module Exposures
88
# A data model for an exposure event
9-
class Event
9+
module Event
1010
TARGETING_KEY_FIELD = 'targeting_key'
11-
ALLOWED_FIELD_TYPES = [
12-
String,
13-
Integer,
14-
Float,
15-
TrueClass,
16-
FalseClass
17-
].freeze
11+
ALLOWED_FIELD_TYPES = [String, Integer, Float, TrueClass, FalseClass].freeze
1812

1913
class << self
14+
def cache_key(result, flag_key:, context:)
15+
"#{flag_key}:#{context.targeting_key}"
16+
end
17+
18+
def cache_value(result, flag_key:, context:)
19+
"#{result.allocation_key}:#{result.variant}"
20+
end
21+
2022
def build(result, flag_key:, context:)
21-
payload = {
23+
{
2224
timestamp: current_timestamp_ms,
2325
allocation: {
2426
key: result.allocation_key
@@ -33,13 +35,13 @@ def build(result, flag_key:, context:)
3335
id: context.targeting_key,
3436
attributes: extract_attributes(context)
3537
}
36-
}
37-
38-
new(payload)
38+
}.freeze
3939
end
4040

4141
private
4242

43+
# NOTE: We take all filds of the context that does not support nesting
44+
# and will ignore targeting key as it will be set as `subject.id`
4345
def extract_attributes(context)
4446
context.fields.select do |key, value|
4547
next false if key == TARGETING_KEY_FIELD
@@ -52,30 +54,6 @@ def current_timestamp_ms
5254
(Datadog::Core::Utils::Time.now.to_f * 1000).to_i
5355
end
5456
end
55-
56-
def initialize(payload)
57-
@payload = payload.freeze
58-
end
59-
60-
def flag_key
61-
@payload.dig(:flag, :key)
62-
end
63-
64-
def targeting_key
65-
@payload.dig(:subject, :id)
66-
end
67-
68-
def allocation_key
69-
@payload.dig(:allocation, :key)
70-
end
71-
72-
def variation_key
73-
@payload.dig(:variant, :key)
74-
end
75-
76-
def to_h
77-
@payload
78-
end
7957
end
8058
end
8159
end

lib/datadog/open_feature/exposures/reporter.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ def initialize(worker, telemetry:, logger:)
1515
@deduplicator = Deduplicator.new
1616
end
1717

18+
# NOTE: Reporting expects evaluation context to be always present, but it
19+
# might be missing depending on the customer way of using flags evaluation API.
20+
# In addition to that the evaluation result must be marked for reporting.
1821
def report(result, flag_key:, context:)
19-
return false unless result.log?
2022
return false if context.nil?
23+
return false unless result.log?
2124

22-
event = Event.build(result, flag_key: flag_key, context: context)
23-
return false if @deduplicator.duplicate?(event)
25+
key = Event.cache_key(result, flag_key: flag_key, context: context)
26+
value = Event.cache_value(result, flag_key: flag_key, context: context)
27+
return false if @deduplicator.duplicate?(key, value)
2428

29+
event = Event.build(result, flag_key: flag_key, context: context)
2530
@worker.enqueue(event)
2631
rescue => e
2732
@logger.debug { "OpenFeature: Failed to report resolution details: #{e.class}: #{e.message}" }
33+
@telemetry.report(e, description: 'OpenFeature: Failed to report resolution details')
2834

2935
false
3036
end

0 commit comments

Comments
 (0)