diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 8c5d1266..629e8e9d 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -497,6 +497,8 @@ def self.polling_payload_to_changeset(data) builder.start(server_intent.payload.code) when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT + next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data]) + begin put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data]) rescue ArgumentError => e @@ -506,6 +508,8 @@ def self.polling_payload_to_changeset(data) builder.add_put(put.kind, put.key, put.version, put.object) when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT + next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data]) + begin delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data]) rescue ArgumentError => e diff --git a/lib/ldclient-rb/impl/data_system/protocolv2.rb b/lib/ldclient-rb/impl/data_system/protocolv2.rb index ef6b5ad3..da1cfd02 100644 --- a/lib/ldclient-rb/impl/data_system/protocolv2.rb +++ b/lib/ldclient-rb/impl/data_system/protocolv2.rb @@ -1,4 +1,5 @@ require 'json' +require 'ldclient-rb/interfaces/data_system' module LaunchDarkly module Impl @@ -9,6 +10,26 @@ module ProtocolV2 # LaunchDarkly data system version 2 (FDv2). # + SUPPORTED_KINDS = [ + LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, + LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, + ].freeze + + # + # Returns true if put-object or delete-object data names a kind this SDK + # does not support. Such objects are ignored so that payloads carrying + # newer kinds can still be applied. + # + # @param data [Hash] The hash representation of the event data + # @return [Boolean] + # + def self.unsupported_kind?(data) + return false unless data.is_a?(Hash) + + kind = data[:kind] + !kind.nil? && !SUPPORTED_KINDS.include?(kind) + end + # # DeleteObject specifies the deletion of a particular object. # diff --git a/lib/ldclient-rb/impl/data_system/streaming.rb b/lib/ldclient-rb/impl/data_system/streaming.rb index ae37bcae..532fef21 100644 --- a/lib/ldclient-rb/impl/data_system/streaming.rb +++ b/lib/ldclient-rb/impl/data_system/streaming.rb @@ -246,13 +246,19 @@ def stop nil when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT - put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(JSON.parse(message.data, symbolize_names: true)) - change_set_builder.add_put(put.kind, put.key, put.version, put.object) + data = JSON.parse(message.data, symbolize_names: true) + unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data) + put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(data) + change_set_builder.add_put(put.kind, put.key, put.version, put.object) + end nil when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT - delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(JSON.parse(message.data, symbolize_names: true)) - change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version) + data = JSON.parse(message.data, symbolize_names: true) + unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data) + delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(data) + change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version) + end nil when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE diff --git a/spec/impl/data_system/polling_payload_parsing_spec.rb b/spec/impl/data_system/polling_payload_parsing_spec.rb index 877ad5ce..e590285a 100644 --- a/spec/impl/data_system/polling_payload_parsing_spec.rb +++ b/spec/impl/data_system/polling_payload_parsing_spec.rb @@ -100,6 +100,28 @@ module DataSystem expect(result.value.selector.version).to eq(461) end + it "ignores put and delete objects of unknown kinds" do + payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","version":112}}},{"event": "put-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461,"object":{"foo":1}}},{"event": "delete-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461}},{"event": "delete-object","data": {"key":"samplesegment","kind":"segment","version":461}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength + + # Parse a payload that mixes known and unknown object kinds. + result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) + + # The payload completes with only the known objects. + expect(result.success?).to eq(true) + expect(result.value.intent_code).to eq(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) + expect(result.value.changes.length).to eq(2) + + expect(result.value.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) + expect(result.value.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) + expect(result.value.changes[0].key).to eq(:sampleflag) + + expect(result.value.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) + expect(result.value.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT) + expect(result.value.changes[1].key).to eq(:samplesegment) + + expect(result.value.selector.state).to eq("(p:5A46PZ79FQ9D08YYKT79DECDNV:461)") + end + it "handles invalid put object" do payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) diff --git a/spec/impl/data_system/streaming_synchronizer_spec.rb b/spec/impl/data_system/streaming_synchronizer_spec.rb index 513c5cf7..860c5029 100644 --- a/spec/impl/data_system/streaming_synchronizer_spec.rb +++ b/spec/impl/data_system/streaming_synchronizer_spec.rb @@ -215,6 +215,72 @@ def initialize(type, data = nil) expect(update.change_set.changes[0].version).to eq(101) end + it "ignores put and delete objects of unknown kinds" do + server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new( + payload: LaunchDarkly::Interfaces::DataSystem::Payload.new( + id: "id", + target: 300, + code: LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL, + reason: "cant-catchup" + ) + ) + known_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( + version: 100, + kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, + key: "flagkey", + object: { key: "flagkey" } + ) + unknown_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( + version: 100, + kind: "some-future-kind", + key: "otherkey", + object: { foo: 1 } + ) + unknown_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( + version: 101, + kind: "some-future-kind", + key: "otherkey" + ) + known_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( + version: 101, + kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, + key: "segmentkey" + ) + selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300) + + expect(logger).not_to receive(:warn) + expect(logger).not_to receive(:error) + + # Process a payload that mixes known and unknown object kinds + synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT, JSON.generate(server_intent.to_h)), +change_set_builder, envid) + [known_put, unknown_put].each do |put| + result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT, JSON.generate(put.to_h)), +change_set_builder, envid) + expect(result).to be_nil + end + [unknown_delete, known_delete].each do |delete_object| + result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT, JSON.generate(delete_object.to_h)), +change_set_builder, envid) + expect(result).to be_nil + end + update = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED, JSON.generate(selector.to_h)), +change_set_builder, envid) + + # The payload completes as valid with only the known objects + expect(update).not_to be_nil + expect(update.state).to eq(LaunchDarkly::Interfaces::DataSource::Status::VALID) + expect(update.error).to be_nil + expect(update.change_set).not_to be_nil + expect(update.change_set.changes.length).to eq(2) + expect(update.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) + expect(update.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) + expect(update.change_set.changes[0].key).to eq(:flagkey) + expect(update.change_set.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) + expect(update.change_set.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT) + expect(update.change_set.changes[1].key).to eq(:segmentkey) + end + it "swallows goodbye events" do server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new( payload: LaunchDarkly::Interfaces::DataSystem::Payload.new(