diff --git a/lib/fmcache/engine.rb b/lib/fmcache/engine.rb index e0effb7..4b80a2b 100644 --- a/lib/fmcache/engine.rb +++ b/lib/fmcache/engine.rb @@ -8,13 +8,15 @@ class Engine # @param [Proc, nil] notifier # @param [#dump#load, nil] json_serializer # @param [String, nil] id_key_prefix + # @param [Proc, nil] save_condition def initialize( client:, fm_parser:, ttl: DEFAULT_TTL, notifier: nil, json_serializer: nil, - id_key_prefix: nil + id_key_prefix: nil, + save_condition: nil ) @client = Client.new(client, notifier) @fm_parser = wrap(fm_parser) @@ -23,6 +25,7 @@ def initialize( @encoder = Encoder.new(@id_key_gen) @decoder = Decoder.new(@fm_parser) @jsonizer = Jsonizer.new(json_serializer) + @save_condition = save_condition end attr_reader :client, :fm_parser, :encoder, :decoder @@ -31,6 +34,7 @@ def initialize( # @param [FieldMaskParser::Node] field_mask # @return [Boolean] def write(values:, field_mask:) + values = values.select { |value| @save_condition.call(value) } if @save_condition normalize!(field_mask) h = encode(values, field_mask) client.set(values: @jsonizer.jsonize(h), ttl: @ttl) @@ -56,10 +60,11 @@ def read(ids:, field_mask:) # @param [] ids # @param [FieldMaskParser::Node] field_mask + # @param [Boolean] skip_write # @yieldparam [, FieldMaskParser::Node] ids, field_mask # @yieldreturn [] # @return [] - def fetch(ids:, field_mask:, &block) + def fetch(ids:, field_mask:, skip_write: false, &block) ids = ids.map(&:to_i) normalize!(field_mask) @@ -68,7 +73,7 @@ def fetch(ids:, field_mask:, &block) # NOTE: get new data d = block.call(incomplete_info.ids, incomplete_info.field_mask) - write(values: d, field_mask: incomplete_info.field_mask) + write(values: d, field_mask: incomplete_info.field_mask) unless skip_write older = encode(incomplete_values, field_mask) newer = encode(d, incomplete_info.field_mask) @@ -80,7 +85,7 @@ def fetch(ids:, field_mask:, &block) else # NOTE: Fallback to block.call with full field_mask d2 = block.call(i_i.ids, field_mask) - write(values: d2, field_mask: field_mask) + write(values: d2, field_mask: field_mask) unless skip_write r = values + d2 end diff --git a/spec/fmcache/engine_spec.rb b/spec/fmcache/engine_spec.rb index 11919fb..7cfa4fc 100644 --- a/spec/fmcache/engine_spec.rb +++ b/spec/fmcache/engine_spec.rb @@ -165,6 +165,51 @@ ] end end + + context "when save_condition is specified" do + let(:id_key_prefix) { nil } + let(:engine) { + FMCache::Engine.new( + client: redis, + fm_parser: fm_parser, + id_key_prefix: id_key_prefix, + save_condition: ->(value) { value[:id] != 2 }, + ) + } + let(:values) { + [ + { id: 1, name: "Taro" }, + { id: 2, name: "Jiro" }, + { id: 3, name: "Saburo" }, + ] + } + let(:fields) { ["id", "name"] } + let(:field_mask) { fm_parser.call(fields) } + + it "filters values based on save_condition" do + engine.write(values: values, field_mask: field_mask) + + expect(redis.hgetall("fmcache:1")).to eq({ + "id" => "[{\"value\":1,\"id\":1,\"p_id\":null}]", + "name" => "[{\"value\":\"Taro\",\"id\":1,\"p_id\":null}]", + }) + expect(redis.hgetall("fmcache:2")).to eq({}) + expect(redis.hgetall("fmcache:3")).to eq({ + "id" => "[{\"value\":3,\"id\":3,\"p_id\":null}]", + "name" => "[{\"value\":\"Saburo\",\"id\":3,\"p_id\":null}]", + }) + end + + it "does not affect read" do + engine.write(values: [values[0], values[2]], field_mask: field_mask) + r = engine.read(ids: [1, 2, 3], field_mask: field_mask) + expect(r).to eq [ + [{ id: 1, name: "Taro" }, { id: 3, name: "Saburo" }], + [], + FMCache::IncompleteInfo.new(ids: [2], field_mask: fm_parser.call(["id", "name"])), + ] + end + end end describe "#read" do @@ -591,6 +636,124 @@ expect(r).to eq [complete_value] end end + + context "when skip_write is true" do + let(:value) { + { + id: 1, + profile: { + id: 3, + introduction: "Hello", + } + } + } + let(:fields) { + [ + "id", + "profile.introduction", + ] + } + let(:field_mask) { fm_parser.call(fields) } + + it "returns data but does not write to cache" do + r = engine.fetch(ids: [1], field_mask: field_mask, skip_write: true) do |_ids, _field_mask| + expect(_ids).to eq [1] + expect(_field_mask.to_paths).to eq field_mask.to_paths + [value] + end + expect(r).to eq [value] + + expect(redis.hgetall("fmcache:1")).to eq({}) + end + + it "does not overwrite existing cache" do + engine.write(values: [value], field_mask: field_mask) + + updated_value = { + id: 1, + profile: { + id: 3, + introduction: "Updated Hello", + } + } + + r = engine.fetch(ids: [1], field_mask: field_mask, skip_write: true) do |_ids, _field_mask| + [updated_value] + end + expect(r).to eq [value] + + r2 = engine.read(ids: [1], field_mask: field_mask) + expect(r2[0]).to eq [value] + end + end + + context "when skip_write is true and fetched value is inconsistent" do + let(:cached_value) { + { + id: 1, + name: "Taro", + profile: { + id: 3, + introduction: "Hello", + } + } + } + let(:fetched_value) { + { + id: 1, + profile: { + id: 4, + schools: [ + { + id: 20, + name: "University of Tokyo", + } + ], + } + } + } + let(:complete_value) { + { + id: 1, + name: "Taro", + profile: { + id: 3, + introduction: "Hello", + schools: [ + { + id: 20, + name: "University of Tokyo", + } + ], + } + } + } + let(:cached_field_mask) { fm_parser.call(["name", "profile.introduction"]) } + let(:read_field_mask) { fm_parser.call(["name", "profile.introduction", "profile.schools.name"]) } + let(:block) { -> {} } + + before do + expect(block).to receive(:call).with([1], fm_parser.call([ + "id", + "profile.id", + "profile.schools.id", + "profile.schools.name", + ])).and_return([fetched_value]) + + expect(block).to receive(:call).with([1], read_field_mask).and_return([complete_value]) + end + + it "returns data but does not write to cache" do + engine.write(values: [cached_value], field_mask: cached_field_mask) + + r = engine.fetch(ids: [1], field_mask: read_field_mask, skip_write: true, &block) + expect(r).to eq [complete_value] + + r2 = engine.read(ids: [1], field_mask: read_field_mask) + expect(r2[0]).to eq [] + expect(r2[1]).to eq [cached_value] + end + end end describe "#delete" do