Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Containers support #16

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
99 changes: 97 additions & 2 deletions lib/cashier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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|
Expand All @@ -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|
Expand Down Expand Up @@ -115,6 +125,7 @@ def keys
# # => ['key1', 'key2', 'key3']
#
def keys_for(tag)
tag = canonize_tags(tag)
adapter.get_fragments_for_tag(tag)
end

Expand Down Expand Up @@ -147,16 +158,100 @@ 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'
require 'cashier/railtie'
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

21 changes: 21 additions & 0 deletions lib/cashier/adapters/cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions lib/cashier/adapters/redis_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/cashier/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Cashier
VERSION = "0.4.1"
VERSION = "0.5.0"
end
30 changes: 30 additions & 0 deletions spec/integration/rails_cache_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
18 changes: 18 additions & 0 deletions spec/lib/cashier/adapters/cache_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions spec/lib/cashier/adapters/redis_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 20 additions & 0 deletions spec/lib/cashier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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