diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/common.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/common.rb index 62b2490..c7cfc40 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/common.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/common.rb @@ -24,8 +24,20 @@ def headers {"Content-Type" => "application/json"}.merge(@custom_headers || {}) end + def evaluation_request(evaluation_context) + ctx = evaluation_context || OpenFeature::SDK::EvaluationContext.new + fields = ctx.fields.dup + # replace targeting_key by targetingKey without mutating original fields + fields["targetingKey"] = ctx.targeting_key + fields.delete("targeting_key") + + {context: fields} + end + def check_retry_after - unless @retry_after.nil? + lock = (@retry_lock ||= Mutex.new) + lock.synchronize do + return if @retry_after.nil? if Time.now < @retry_after raise OpenFeature::GoFeatureFlag::RateLimited.new(nil) else @@ -109,12 +121,18 @@ def parse_retry_later_header(response) return nil if retry_after.nil? begin - @retry_after = if /^\d+$/.match?(retry_after) - # Retry-After is in seconds - Time.now + Integer(retry_after) - else - # Retry-After is an HTTP-date - Time.httpdate(retry_after) + next_retry_time = + if /^\d+$/.match?(retry_after) + # Retry-After is in seconds + Time.now + Integer(retry_after) + else + # Retry-After is an HTTP-date + Time.httpdate(retry_after) + end + # Protect updates and never shorten an existing backoff window + lock = (@retry_lock ||= Mutex.new) + lock.synchronize do + @retry_after = [@retry_after, next_retry_time].compact.max end rescue ArgumentError # ignore invalid Retry-After header diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/http_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/http_api.rb index ef85cbb..dceaed8 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/http_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/http_api.rb @@ -19,13 +19,10 @@ def initialize(endpoint: nil, custom_headers: nil, instrumentation: nil) def evaluate_ofrep_api(flag_key:, evaluation_context:) check_retry_after - evaluation_context = OpenFeature::SDK::EvaluationContext.new if evaluation_context.nil? - # replace targeting_key by targetingKey - evaluation_context.fields["targetingKey"] = evaluation_context.targeting_key - evaluation_context.fields.delete("targeting_key") + request = evaluation_request(evaluation_context) response = @faraday_connection.post("/ofrep/v1/evaluate/flags/#{flag_key}") do |req| - req.body = {context: evaluation_context.fields}.to_json + req.body = request.to_json end case response.status diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/unix_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/unix_api.rb index b2c9d31..169f837 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/unix_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/client/unix_api.rb @@ -7,21 +7,17 @@ module OpenFeature module GoFeatureFlag module Client class UnixApi < Common - attr_accessor :socket - - def initialize(endpoint: nil, custom_headers: nil) + def initialize(endpoint: nil, custom_headers: nil, unix_socket_client_factory: nil) @custom_headers = custom_headers - @socket = HttpUnix.new(endpoint) + @endpoint = endpoint + @unix_socket_client_factory = unix_socket_client_factory || ->(ep) { HttpUnix.new(ep) } end def evaluate_ofrep_api(flag_key:, evaluation_context:) check_retry_after - evaluation_context = OpenFeature::SDK::EvaluationContext.new if evaluation_context.nil? - # replace targeting_key by targetingKey - evaluation_context.fields["targetingKey"] = evaluation_context.targeting_key - evaluation_context.fields.delete("targeting_key") - response = @socket.post("/ofrep/v1/evaluate/flags/#{flag_key}", {context: evaluation_context.fields}, headers) + request = evaluation_request(evaluation_context) + response = thread_local_socket.post("/ofrep/v1/evaluate/flags/#{flag_key}", request, headers) case response.code when "200" @@ -39,6 +35,13 @@ def evaluate_ofrep_api(flag_key:, evaluation_context:) raise OpenFeature::GoFeatureFlag::InternalServerError.new(response) end end + + private + + def thread_local_socket + key = "openfeature_goff_unix_socket_#{object_id}" + Thread.current[key] ||= @unix_socket_client_factory.call(@endpoint) + end end end end diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/client/unix_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/client/unix_api_spec.rb index 80fec13..9d1f3c4 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/client/unix_api_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/client/unix_api_spec.rb @@ -2,8 +2,10 @@ RSpec.describe OpenFeature::GoFeatureFlag::Client::UnixApi do subject(:unix_api) do - described_class.new(endpoint: "/tmp/http.sock") + described_class.new(endpoint: "/tmp/http.sock", unix_socket_client_factory: unix_socket_client_factory) end + let(:unix_socket_client) { instance_double(HttpUnix) } + let(:unix_socket_client_factory) { ->(_endpoint) { unix_socket_client } } let(:default_evaluation_context) do OpenFeature::SDK::EvaluationContext.new( @@ -20,7 +22,7 @@ it "should raise an error if rate limited" do allow(response).to receive(:code).and_return("429") allow(response).to receive(:[]).with("Retry-After").and_return(nil) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -29,7 +31,7 @@ it "should raise an error if not authorized (401)" do allow(response).to receive(:code).and_return("401") - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -38,7 +40,7 @@ it "should raise an error if not authorized (403)" do allow(response).to receive(:code).and_return("403") - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -47,7 +49,7 @@ it "should raise an error if flag not found (404)" do allow(response).to receive(:code).and_return("404") - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "does-not-exists", evaluation_context: default_evaluation_context) @@ -56,7 +58,7 @@ it "should raise an error if unknown http code (500)" do allow(response).to receive(:code).and_return("500") - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -71,7 +73,7 @@ } allow(response).to receive(:code).and_return("400") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) got = unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) want = OpenFeature::GoFeatureFlag::OfrepApiResponse.new( @@ -96,7 +98,7 @@ } allow(response).to receive(:code).and_return("200") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) got = unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) want = OpenFeature::GoFeatureFlag::OfrepApiResponse.new( @@ -120,7 +122,7 @@ } allow(response).to receive(:code).and_return("200") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -136,7 +138,7 @@ } allow(response).to receive(:code).and_return("200") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -152,7 +154,7 @@ } allow(response).to receive(:code).and_return("200") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -168,7 +170,7 @@ } allow(response).to receive(:code).and_return("200") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -181,7 +183,7 @@ } allow(response).to receive(:code).and_return("400") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -192,7 +194,7 @@ body = {key: "double_key"} allow(response).to receive(:code).and_return("400") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -202,7 +204,7 @@ it "should not be able to call the API again if rate-limited (with retry-after int)" do allow(response).to receive(:code).and_return("429") allow(response).to receive(:[]).with("Retry-After").and_return("10") - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -224,7 +226,7 @@ allow(response).to receive(:code).and_return("429", "200") allow(response).to receive(:[]).with("Retry-After").and_return("1") allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -240,7 +242,7 @@ it "should not be able to call the API again if rate-limited (with retry-after date)" do allow(response).to receive(:code).and_return("429") allow(response).to receive(:[]).with("Retry-After").and_return((Time.now + 1).httpdate) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) @@ -262,7 +264,7 @@ allow(response).to receive(:code).and_return("429", "200") allow(response).to receive(:[]).with("Retry-After").and_return((Time.now + 1).httpdate) allow(response).to receive(:body).and_return(body.to_json) - allow(unix_api.socket).to receive(:post).and_return(response) + allow(unix_socket_client).to receive(:post).and_return(response) expect { unix_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context)