From 31895af048cecc59bbe84cde02967ec35e8b236a Mon Sep 17 00:00:00 2001 From: Yuval Larom Date: Mon, 21 Jan 2013 00:09:06 +0200 Subject: [PATCH 1/3] add containers to redis store --- lib/cashier/adapters/redis_store.rb | 16 ++++++++++++++++ spec/lib/cashier/adapters/redis_store_spec.rb | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/cashier/adapters/redis_store.rb b/lib/cashier/adapters/redis_store.rb index 32f27fc..83129c0 100644 --- a/lib/cashier/adapters/redis_store.rb +++ b/lib/cashier/adapters/redis_store.rb @@ -41,6 +41,22 @@ def self.clear def self.keys tags.inject([]) { |arry, tag| arry += get_fragments_for_tag(tag) }.compact end + + def self.get_tags_containers(tags) + all_containers = [] + cache_keys = tags.map { |tag| Cashier::container_cache_key(tag) } + all_containers = redis.sunion(*cache_keys) + return all_containers + end + + def self.add_tags_containers(tags, containers) + return if !containers || containers.empty? + tags.each do |tag| + cache_key = Cashier::container_cache_key(tag) + redis.sadd(cache_key, containers.flatten) + end + end + end end end diff --git a/spec/lib/cashier/adapters/redis_store_spec.rb b/spec/lib/cashier/adapters/redis_store_spec.rb index be0c974..492fc8a 100644 --- a/spec/lib/cashier/adapters/redis_store_spec.rb +++ b/spec/lib/cashier/adapters/redis_store_spec.rb @@ -86,4 +86,22 @@ subject.keys.sort.should eql(%w(key1 key2 key3)) end end + + context "containers" do + it "should be added to tags" do + subject.add_tags_containers(['key1', 'key2'], ['container1', 'container2']) + subject.add_tags_containers(['key1', 'key3'], ['container2', 'container3']) + + subject.get_tags_containers(['key1']).should == ['container1', 'container2', 'container3'] + subject.get_tags_containers(['key2']).should == ['container1', 'container2'] + subject.get_tags_containers(['key1', 'key2']).should == ['container1', 'container2', 'container3'] + end + + it "should be returned for tags" do + subject.add_tags_containers(['key1', 'key2'], ['container1', 'container2']) + subject.get_tags_containers(['key1']).should == ['container1', 'container2'] + subject.get_tags_containers(['key2']).should == ['container1', 'container2'] + end + end + end From eee3b55d5b363562182d71578d1af5ed1109549d Mon Sep 17 00:00:00 2001 From: Yuval Larom Date: Mon, 21 Jan 2013 00:09:52 +0200 Subject: [PATCH 2/3] add containers to cache store --- lib/cashier/adapters/cache_store.rb | 21 +++++++++++++++++++ spec/lib/cashier/adapters/cache_store_spec.rb | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/cashier/adapters/cache_store.rb b/lib/cashier/adapters/cache_store.rb index 25b183b..83494cc 100644 --- a/lib/cashier/adapters/cache_store.rb +++ b/lib/cashier/adapters/cache_store.rb @@ -8,6 +8,7 @@ def self.store_fragment_in_tag(fragment, tag) end def self.store_tags(tags) + cashier_tags = Rails.cache.fetch(Cashier::CACHE_KEY) || [] cashier_tags = (cashier_tags + tags).uniq @@ -40,6 +41,26 @@ def self.clear def self.keys tags.inject([]) { |arry, tag| arry += Rails.cache.fetch(tag) }.compact end + + def self.get_tags_containers(tags) + all_containers = [] + tags.each do |tag| + cache_key = Cashier::container_cache_key(tag) + containers = Rails.cache.fetch(cache_key) || [] + all_containers += containers + end + return all_containers.flatten.uniq + end + + def self.add_tags_containers(tags, containers) + tags.each do |tag| + cache_key = Cashier::container_cache_key(tag) + existing_containers = Rails.cache.fetch(cache_key) || [] + all_containers = (existing_containers + containers).flatten.uniq + Rails.cache.write(cache_key, all_containers) + end + end + end end end diff --git a/spec/lib/cashier/adapters/cache_store_spec.rb b/spec/lib/cashier/adapters/cache_store_spec.rb index f7e0faa..794ce58 100644 --- a/spec/lib/cashier/adapters/cache_store_spec.rb +++ b/spec/lib/cashier/adapters/cache_store_spec.rb @@ -71,4 +71,22 @@ subject.keys.should eql(%w(key1 key2 key3)) end end + + context "containers" do + it "should be added to tags" do + subject.add_tags_containers(['key1', 'key2'], ['container1', 'container2']) + cache.fetch('cashier-tag-containers:key1').should == ['container1', 'container2'] + cache.fetch('cashier-tag-containers:key2').should == ['container1', 'container2'] + + subject.add_tags_containers(['key1', 'key3'], ['container3']) + cache.fetch('cashier-tag-containers:key1').should == ['container1', 'container2', 'container3'] + cache.fetch('cashier-tag-containers:key3').should == ['container3'] + end + + it "should be returned for tags" do + subject.add_tags_containers(['key1', 'key2'], ['container1', 'container2']) + subject.get_tags_containers(['key1']).should == ['container1', 'container2'] + subject.get_tags_containers(['key2']).should == ['container1', 'container2'] + end + end end From 01afb86753ca4a24a004905f420393d3dedbea64 Mon Sep 17 00:00:00 2001 From: Yuval Larom Date: Mon, 21 Jan 2013 00:15:06 +0200 Subject: [PATCH 3/3] use adapters container functions, testing, canonize keys --- lib/cashier.rb | 99 ++++++++++++++++++- lib/cashier/version.rb | 2 +- .../rails_cache_integration_spec.rb | 30 ++++++ spec/lib/cashier_spec.rb | 20 ++++ 4 files changed, 148 insertions(+), 3 deletions(-) diff --git a/lib/cashier.rb b/lib/cashier.rb index bdd9c35..88fe06f 100644 --- a/lib/cashier.rb +++ b/lib/cashier.rb @@ -2,6 +2,10 @@ module Cashier CACHE_KEY = 'cashier-tags' + def self.container_cache_key(tag) + "cashier-tag-containers:#{tag}" + end + class << self # Public: whether the module will perform caching or not. this is being set in the application layer .perform_caching configuration @@ -28,6 +32,7 @@ def store_fragment(fragment, *tags) return unless perform_caching? tags = tags.flatten + tags = canonize_tags(tags) ActiveSupport::Notifications.instrument("store_fragment.cashier", :data => [fragment, tags]) do tags.each do |tag| @@ -51,6 +56,11 @@ def store_fragment(fragment, *tags) def expire(*tags) return unless perform_caching? + # add tags of container fragments to expired tags list + tags = canonize_tags(tags) + containers = adapter.get_tags_containers(tags) || [] + tags = (tags + containers).compact.uniq + ActiveSupport::Notifications.instrument("expire.cashier", :data => tags) do # delete them from the cache tags.each do |tag| @@ -115,6 +125,7 @@ def keys # # => ['key1', 'key2', 'key3'] # def keys_for(tag) + tag = canonize_tags(tag) adapter.get_fragments_for_tag(tag) end @@ -147,7 +158,75 @@ def adapter def adapter=(cache_adapter) @@adapter = cache_adapter end + + # Public: add tags of a container fragment into the current container stack (used internally by ActiveSupport::Notifications) + # + # cache_adapter - :cache_store / :redis_store + # + # Examples + # + # Cashier.push_container(['section2']) + # + def push_container(*tags) + return unless perform_caching? + @@container_stack ||= [] + tags = canonize_tags(tags) + adapter.add_tags_containers(tags, @@container_stack) + @@container_stack.push tags + end + + # Public: remove tags of a container fragment from the current container stack + # + # cache_adapter - :cache_store / :redis_store + # + # Examples + # + # Cashier.pop_container() + # + def pop_container() + return unless perform_caching? + @@container_stack ||= [] + container = @@container_stack.pop + container + end + + # Public: get the tags of containers for the given fragment tags + # + # cache_adapter - :cache_store / :redis_store + # + # Examples + # + # Cashier.get_containers(['article1']) + # # => ['section2', 'section3'] + # + def get_containers(tags) + tags = canonize_tags(tags) + adapter.get_tags_containers(tags) + end + + + # Public: canonize tags: convert ActiveRecord objects to string (inc. id) + # + # cache_adapter - :cache_store / :redis_store + # + # Examples + # + # Cashier.canonize_tags([1, :a, Article.find(123)]) + # # => [1, :a, "Article-123"] + # + def canonize_tags(tags) + tags = [tags || []].flatten + tags.map do |tag| + if tag.is_a?(ActiveRecord::Base) + "#{tag.class.name}-#{tag.to_param}" + else + tag + end + end + end + end + end require 'rails' @@ -155,8 +234,24 @@ def adapter=(cache_adapter) require 'cashier/adapters/cache_store' require 'cashier/adapters/redis_store' -# Connect cashier up to the low level Rails cache. +# Connect cashier up to the low level Rails cache: + +# When Rails cache is missing a fragment, it is going to be rendered - add its tags to the container stack +ActiveSupport::Notifications.subscribe("cache_read.active_support") do |*args| + payload = ActiveSupport::Notifications::Event.new(*args).payload + tag = payload[:tag] + # if not a cache hit, we're going to build the fragment - add to container stack + Cashier.push_container(*tag) if tag && !payload[:hit] +end + +# When a fragment was written into Rails cache it is now rendered and done - remove its tags from the container stack ActiveSupport::Notifications.subscribe("cache_write.active_support") do |*args| payload = ActiveSupport::Notifications::Event.new(*args).payload - Cashier.store_fragment payload[:key], payload[:tag] if payload[:tag] + tag = payload[:tag] + + if tag + Cashier.store_fragment(payload[:key], tag) + Cashier.pop_container + end end + diff --git a/lib/cashier/version.rb b/lib/cashier/version.rb index 7dea6ab..4e4ffb3 100644 --- a/lib/cashier/version.rb +++ b/lib/cashier/version.rb @@ -1,3 +1,3 @@ module Cashier - VERSION = "0.4.1" + VERSION = "0.5.0" end diff --git a/spec/integration/rails_cache_integration_spec.rb b/spec/integration/rails_cache_integration_spec.rb index 231a177..dc3af39 100644 --- a/spec/integration/rails_cache_integration_spec.rb +++ b/spec/integration/rails_cache_integration_spec.rb @@ -4,6 +4,10 @@ subject { Rails.cache } let(:cashier) { Cashier } + before(:each) do + Cashier.adapter = :cache_store + end + it "should ensure that cache operations are instrumented" do ActiveSupport::Cache::Store.instrument.should be_true end @@ -35,4 +39,30 @@ subject.fetch("foo") { "bar" } end end + + context "read" do + it "should keep track of fragment container hierarchy" do + subject.fetch("foo1", :tag => ["some_tag", "some_other_tag"]) do + subject.fetch("foo2", :tag => ["some_inner_tag", "some_other_inner_tag"]) { "bar" } + end + + cashier.get_containers(["some_inner_tag"]).should == ["some_tag", "some_other_tag"] + cashier.get_containers(["some_other_inner_tag"]).should == ["some_tag", "some_other_tag"] + end + end + + context "expire" do + let(:notification_system) { ActiveSupport::Notifications } + + it "should expire containers when expiring a tag" do + subject.fetch("foo3", :tag => ["outer_tag1", "outer_tag2"]) do + subject.fetch("foo4", :tag => ["inner_tag1", "inner_tag2"]) { "bar" } + end + cashier.adapter.get_fragments_for_tag("outer_tag1").should == ["foo3"] + + cashier.expire("inner_tag1") + cashier.adapter.get_fragments_for_tag("outer_tag1").should == [] + end + end + end diff --git a/spec/lib/cashier_spec.rb b/spec/lib/cashier_spec.rb index 7c72229..f6c6488 100644 --- a/spec/lib/cashier_spec.rb +++ b/spec/lib/cashier_spec.rb @@ -48,6 +48,7 @@ end it "should raise a callback method when I call expire" do + notification_system.should_receive(:instrument).with("cache_read.active_support", :key => "cashier-tag-containers:some_tag") notification_system.should_receive(:instrument).with("expire.cashier", :data => ["some_tag"]) subject.expire("some_tag") end @@ -131,4 +132,23 @@ it "shold allow to get the adapter" do subject.respond_to?(:adapter).should be_true end + + it "should set a container cache key" do + subject.container_cache_key(:something).should match(/something/) + end + + it "should canonize ActiveRecord tags" do + require 'active_record' + + ar_class = double("ar_class") + ar_class.stub(name: :AR) + + ar = double("active_record") + ar.stub(class: ar_class) + ar.stub(is_a?: true) + ar.stub(to_param: 123) + + res = Cashier.canonize_tags([1, 2, ar]) + res.should == [1, 2, "AR-123"] + end end