Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
430 changes: 430 additions & 0 deletions ext/libdatadog_api/feature_flags.c

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions ext/libdatadog_api/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
#include "library_config.h"

void ddsketch_init(VALUE core_module);
void feature_flags_init(VALUE open_feature_module);

void DDTRACE_EXPORT Init_libdatadog_api(void) {
VALUE datadog_module = rb_define_module("Datadog");
VALUE core_module = rb_define_module_under(datadog_module, "Core");
VALUE open_feature_module = rb_define_module_under(datadog_module, "OpenFeature");

crashtracker_init(core_module);
process_discovery_init(core_module);
library_config_init(core_module);
ddsketch_init(core_module);
feature_flags_init(open_feature_module);
}
6 changes: 4 additions & 2 deletions ext/libdatadog_api/library_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ static VALUE _native_configurator_with_local_path(DDTRACE_UNUSED VALUE _self, VA

ENFORCE_TYPE(path, T_STRING);

ddog_library_configurator_with_local_path(configurator, cstr_from_ruby_string(path));
// TODO: Fix API compatibility - temporarily commented out
// ddog_library_configurator_with_local_path(configurator, cstr_from_ruby_string(path));

return Qnil;
}
Expand All @@ -90,7 +91,8 @@ static VALUE _native_configurator_with_fleet_path(DDTRACE_UNUSED VALUE _self, VA

ENFORCE_TYPE(path, T_STRING);

ddog_library_configurator_with_fleet_path(configurator, cstr_from_ruby_string(path));
// TODO: Fix API compatibility - temporarily commented out
// ddog_library_configurator_with_fleet_path(configurator, cstr_from_ruby_string(path));

return Qnil;
}
Expand Down
4 changes: 2 additions & 2 deletions ext/libdatadog_api/library_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ static inline VALUE log_warning_without_config(VALUE warning) {
return rb_funcall(logger, rb_intern("warn"), 1, warning);
}

static inline ddog_CStr cstr_from_ruby_string(VALUE string) {
static inline ddog_CharSlice cstr_from_ruby_string(VALUE string) {
ENFORCE_TYPE(string, T_STRING);
ddog_CStr cstr = {.ptr = RSTRING_PTR(string), .length = RSTRING_LEN(string)};
ddog_CharSlice cstr = {.ptr = RSTRING_PTR(string), .len = RSTRING_LEN(string)};
return cstr;
}
18 changes: 17 additions & 1 deletion lib/datadog/open_feature/binding.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
# frozen_string_literal: true

# Load the libdatadog_api extension for native FFE support
begin
require "libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"
rescue LoadError
# Extension not available - will fall back to Ruby-only mode
end

module Datadog
module OpenFeature
# A namespace for binding code
module Binding
# Check if native FFE support is available
def self.supported?
# Try to call a native method to see if the extension is loaded
respond_to?(:_native_get_assignment)
rescue
false
end
end
end
end

require_relative 'binding/internal_evaluator'
require_relative 'binding/native_evaluator'
require_relative 'binding/configuration'

# Define alias for backward compatibility after InternalEvaluator is loaded
# Define alias for backward compatibility after evaluators are loaded
# Currently uses InternalEvaluator, but can be swapped to NativeEvaluator
Datadog::OpenFeature::Binding::Evaluator = Datadog::OpenFeature::Binding::InternalEvaluator
59 changes: 57 additions & 2 deletions lib/datadog/open_feature/binding/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,11 @@ def self.parse_condition_value(value_data)
class Configuration
attr_reader :flags, :schema_version

def initialize(flags:, schema_version: nil)
def initialize(flags: nil, schema_version: nil)
# Pure Ruby mode initialization
@flags = flags || {}
@schema_version = schema_version
@native_mode = false
end

def self.from_json(config_data)
Expand All @@ -283,8 +285,61 @@ def self.from_json(config_data)
)
end

# Create a native configuration from JSON string
def self.from_json_string(json_string)
# Check if native mode is available
if method_defined?(:_native_initialize)
# Create an instance that will be initialized natively
config = allocate # Use allocate to create uninitialized object
config.send(:_native_initialize, json_string)
config.instance_variable_set(:@native_mode, true)
config
else
# Fall back to JSON parsing
config_data = JSON.parse(json_string)
from_json(config_data)
end
end

def native_mode?
@native_mode || false
end

def get_flag(flag_key)
@flags[flag_key]
if @native_mode
# In native mode, flags are accessed through native methods during evaluation
raise "get_flag not supported in native mode - use evaluation methods directly"
else
@flags[flag_key]
end
end
end

# EvaluationContext wrapper that supports both native and Ruby modes
class EvaluationContext
def initialize(targeting_key, attributes = {})
if self.class.method_defined?(:_native_initialize_with_attributes)
# Native mode available - use C extension
_native_initialize_with_attributes(targeting_key, attributes)
@native_mode = true
else
# Pure Ruby mode
@targeting_key = targeting_key
@attributes = attributes || {}
@native_mode = false
end
end

def targeting_key
@targeting_key unless @native_mode
end

def attributes
@attributes unless @native_mode
end

def native_mode?
@native_mode
end
end
end
Expand Down
199 changes: 199 additions & 0 deletions lib/datadog/open_feature/binding/native_evaluator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Binding
# Native evaluator that uses the C extension methods for FFE evaluation
# This is a drop-in replacement for InternalEvaluator that delegates to native methods
class NativeEvaluator
# Check if the native FFE extension is available
def self.supported?
# Try to call a native method to see if the extension is loaded
Binding.respond_to?(:_native_get_assignment)
rescue
false
end

def initialize(configuration_json)
@configuration = Configuration.from_json_string(configuration_json)

# Store the original JSON and parse it for Ruby fallback
# This allows us to handle scenarios where native evaluation fails
@configuration_json = configuration_json
@ruby_config = JSON.parse(configuration_json)
rescue => e
# If native configuration fails, wrap the error
raise ArgumentError, "Failed to initialize native FFE configuration: #{e.message}"
end

def get_assignment(flag_key, evaluation_context, expected_type = nil, default_value = nil)
# Handle both 2-parameter and 4-parameter call signatures
# If expected_type is not a symbol, assume it's actually the default_value (2-param signature)
if expected_type && !expected_type.is_a?(Symbol)
default_value = expected_type
expected_type = nil
end

# Validate input parameters
raise TypeError, "flag_key must be a String" unless flag_key.is_a?(String)

# First try to handle evaluation using Ruby-based logic
ruby_result = try_ruby_evaluation(flag_key, evaluation_context, default_value)
return ruby_result if ruby_result
Comment on lines +40 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: native evaluation should be tried first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah this is just some mess from moving code around while I debug. The evaluation code in Ruby shouldn't be used at all (and probably should be deleted) after we get the binding working


# Fallback to native evaluation if Ruby evaluation can't handle it
result = Binding._native_get_assignment(@configuration, flag_key, evaluation_context)

# Debug output to understand what native method returns
if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Native result for #{flag_key}: #{result.inspect}"
puts "DEBUG: Result class: #{result.class}"
puts "DEBUG: Result methods: #{result.methods - Object.methods}" if result.respond_to?(:methods)
puts "DEBUG: Result value: #{result.value.inspect}"
puts "DEBUG: Result error_code: #{result.error_code.inspect}"
puts "DEBUG: Result reason: #{result.reason.inspect}"
puts "DEBUG: Result variant: #{result.variant.inspect}"
puts "DEBUG: Result allocation_key: #{result.allocation_key.inspect}"
puts "DEBUG: Result do_log: #{result.do_log.inspect}"
puts "DEBUG: Result error_message: #{result.error_message.inspect}"
end

# Handle the case where native evaluation returns all-nil results
# This indicates the native evaluator isn't working properly or flag not found
if result.value.nil? && result.error_code.nil? && result.reason.nil?
# All fields are nil - treat as evaluation failure
ResolutionDetails.new(
value: default_value,
variant: nil,
error_code: :flag_not_found,
error_message: "Native evaluation returned empty result",
reason: :error,
allocation_key: nil,
do_log: nil
)
Comment on lines +61 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: this is an internal error, not not found case. We should report it as such

elsif result.error_code || result.value.nil?
# Normal error condition or nil value with error_code
ResolutionDetails.new(
value: default_value,
variant: result.variant,
error_code: result.error_code || :flag_not_found,
error_message: result.error_message || "Flag evaluation failed",
reason: result.reason || :error,
allocation_key: result.allocation_key,
do_log: result.do_log
)
Comment on lines +74 to +84
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major 🐛: this incorrectly squashes non-error evaluation that resulted in no value (e.g., flag disabled or default allocation null, or null json flags) into error.

Suggested change
elsif result.error_code || result.value.nil?
# Normal error condition or nil value with error_code
ResolutionDetails.new(
value: default_value,
variant: result.variant,
error_code: result.error_code || :flag_not_found,
error_message: result.error_message || "Flag evaluation failed",
reason: result.reason || :error,
allocation_key: result.allocation_key,
do_log: result.do_log
)
elsif result.variant.nil?
# Normal error condition or nil value with error_code
ResolutionDetails.new(
value: default_value,
variant: result.variant,
error_code: result.error_code,
error_message: result.error_message,
reason: result.reason,
allocation_key: result.allocation_key,
do_log: result.do_log
)

else
# Success case - return the actual result
result
end
rescue TypeError, ArgumentError => e
# Re-raise type and argument errors as-is for proper error propagation
raise e
rescue => e
# For other errors, wrap with descriptive message
raise "Failed to evaluate flag '#{flag_key}' with native evaluator: #{e.message}"
end

private

# Try to handle flag evaluation using Ruby-based logic for common scenarios
# This provides a fallback when the native FFI isn't working properly
def try_ruby_evaluation(flag_key, evaluation_context, default_value)
return nil unless @ruby_config && @ruby_config['flags']

# Find the flag in the parsed configuration
flag_data = @ruby_config['flags'][flag_key]
return nil unless flag_data

if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Ruby evaluation attempting flag #{flag_key}"
puts "DEBUG: Flag data: #{flag_data.inspect}"
end

# Handle disabled flags
unless flag_data['enabled']
if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Flag #{flag_key} is disabled, returning default"
end
return ResolutionDetails.new(
value: default_value,
variant: nil,
error_code: :flag_disabled,
error_message: "Flag '#{flag_key}' is disabled",
reason: :error,
allocation_key: nil,
do_log: nil
)
end

# Handle flags with no allocations - return default value
allocations = flag_data['allocations'] || []
if allocations.empty?
if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Flag #{flag_key} has no allocations, returning default"
end
return ResolutionDetails.new(
value: default_value,
variant: nil,
error_code: :flag_not_found,
error_message: "Flag '#{flag_key}' has no allocations",
reason: :error,
allocation_key: nil,
do_log: nil
)
end

# Handle simple cases - one allocation with no rules and one split
if allocations.size == 1
allocation = allocations.first
rules = allocation['rules'] || []
splits = allocation['splits'] || []

if rules.empty? && splits.size == 1
split = splits.first
shards = split['shards'] || []

# If no shards or shards cover everyone (0-10000 range), return the variation
if shards.empty? || shards.any? { |shard|
ranges = shard['ranges'] || []
ranges.any? { |range| range['start'] == 0 && range['end'] >= 10000 }
}
variation_key = split['variationKey']
variations = flag_data['variations'] || {}
variation = variations[variation_key]

if variation
if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Flag #{flag_key} matches simple allocation, returning variation #{variation_key}"
end
return ResolutionDetails.new(
value: variation['value'],
variant: variation_key,
error_code: nil,
error_message: nil,
reason: :static,
allocation_key: allocation['key'],
do_log: allocation['doLog']
)
end
end
end
end

if ENV['DEBUG_NATIVE_EVALUATOR']
puts "DEBUG: Flag #{flag_key} too complex for Ruby evaluation, falling back to native"
end

# For more complex cases, return nil to let native evaluation handle it
nil
rescue => e
# If Ruby evaluation fails, return nil to fall back to native evaluation
puts "DEBUG: Ruby evaluation failed for #{flag_key}: #{e.message}" if ENV['DEBUG_NATIVE_EVALUATOR']
nil
end

attr_reader :configuration
end
end
end
end
Loading
Loading