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
13 changes: 9 additions & 4 deletions lib/fmcache/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -56,10 +60,11 @@ def read(ids:, field_mask:)

# @param [<Integer | String>] ids
# @param [FieldMaskParser::Node] field_mask
# @param [Boolean] skip_write
# @yieldparam [<Integer>, FieldMaskParser::Node] ids, field_mask
# @yieldreturn [<Hash>]
# @return [<Hash>]
def fetch(ids:, field_mask:, &block)
def fetch(ids:, field_mask:, skip_write: false, &block)
ids = ids.map(&:to_i)
normalize!(field_mask)

Expand All @@ -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)
Expand All @@ -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

Expand Down
163 changes: 163 additions & 0 deletions spec/fmcache/engine_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down