cachex 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +103 -0
- data/Rakefile +2 -0
- data/cachex.gemspec +17 -0
- data/lib/cachex.rb +70 -0
- data/lib/cachex/adapters/redis_store.rb +8 -0
- data/lib/cachex/config.rb +24 -0
- data/lib/cachex/node.rb +27 -0
- data/lib/cachex/railtie.rb +7 -0
- data/lib/cachex/version.rb +3 -0
- data/lib/cachex/view_helpers.rb +7 -0
- data/lib/generators/cachex/install_generator.rb +12 -0
- data/lib/generators/templates/cachex.rb +4 -0
- metadata +60 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Milovan Zogovic
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Cachex
|
2
|
+
|
3
|
+
This gem is only proof of concept and it is not safe for use in production.
|
4
|
+
|
5
|
+
## Idea
|
6
|
+
|
7
|
+
This gem is inspired by excellent [Cashier](https://github.com/twinturbo/cashier) gem. I wanted to take it a step further and try to automate tag dependency generation.
|
8
|
+
|
9
|
+
I had this scenario in mind:
|
10
|
+
|
11
|
+
* Blog has many posts
|
12
|
+
* Each post has an author (user)
|
13
|
+
* Each post has many comments
|
14
|
+
* Each comment has its own author (user)
|
15
|
+
|
16
|
+
Each model has its own partial. Inside `_post` partial we have `_user` partial that renders post's author. `_post` partial also include many `_comment` partials. Each `_comment` partial have author's name displayed in it.
|
17
|
+
|
18
|
+
We're also using fragment caching in order to cache entire post for faster serving. The burning question is: **what happens if user decides to change its name (or perhaps, more commonly, an avatar)?**. We have to invalidate every cached partial that displayed that user. How do we do that?
|
19
|
+
|
20
|
+
If we're using auto-expiring key strategy the only way to accomplish this would be to `touch` **everything** that has to do with that user. That just doesn't make any sense.
|
21
|
+
|
22
|
+
If we're using tag-based strategy, then we would need to manually tag every fragment with all users that have to do something with it (even users from its comments). Such approach is going to be very hard when we get to deeply nested partials. Top most partial will have to be aware of **all dependencies** down to the deepest one.
|
23
|
+
|
24
|
+
And finally, my solution: **to automatically extract all dependencies from all sub-partials and to store them to help cache invalidation**.
|
25
|
+
|
26
|
+
## Example
|
27
|
+
|
28
|
+
Here is how the example above is implemented with `cachex`.
|
29
|
+
|
30
|
+
We have `_post` partial defined like this:
|
31
|
+
|
32
|
+
|
33
|
+
<%= cachex dom_id(post) do %>
|
34
|
+
<article class="post">
|
35
|
+
<h1><%= post.title %></h1>
|
36
|
+
<p>Author: <%= render post.user %></p>
|
37
|
+
|
38
|
+
<section class="comments">
|
39
|
+
<h3>Comments:</h3>
|
40
|
+
<%= render post.comments %>
|
41
|
+
</section>
|
42
|
+
</article>
|
43
|
+
<% end %>
|
44
|
+
|
45
|
+
Please note that instead of using `cache` we're using `cachex` helper method. We need to pass at least one argument which is the key for given record. It is good practice to use something that can be easily generated and parsed, so `dom_id` method will do the job.
|
46
|
+
|
47
|
+
The rest is just good old rails partial. From it, we're rendering `_user` partial to output post author, and multiple `_comment` partials.
|
48
|
+
|
49
|
+
Here is how `_user` partial looks like:
|
50
|
+
|
51
|
+
|
52
|
+
<%= cachex dom_id(user) do %>
|
53
|
+
<span class="user"><%= user.name %></span>
|
54
|
+
<% end %>
|
55
|
+
|
56
|
+
Again, nothing special to it. Just displaying the user name.
|
57
|
+
|
58
|
+
And, here is how the `_comment` partial looks like:
|
59
|
+
|
60
|
+
|
61
|
+
<%= cachex dom_id(comment), "user_#{comment.user_id}" do %>
|
62
|
+
<article class="comment">
|
63
|
+
<p><%= comment.body %></p>
|
64
|
+
<p>Author: <%= comment.user.name %></p>
|
65
|
+
</article>
|
66
|
+
<% end %>
|
67
|
+
|
68
|
+
You notice that we've passed second parameter to `cachex` call. Actually, you may pass as many as you like. These are keys that current partial depends on. In our case it depends on comment's author. We could have just rendered the user partial here as well, but just wanted to demonstrate that you can also pass dependencies manually.
|
69
|
+
|
70
|
+
Now comes the magic. First time this gets rendered, post will aggregate all dependencies from all sub-partials. In our case post will be tagged with following dependencies:
|
71
|
+
|
72
|
+
* It's author
|
73
|
+
* All of its comments
|
74
|
+
* All of its comment authors
|
75
|
+
|
76
|
+
**If any of these keys expire, post will expire as well!**
|
77
|
+
|
78
|
+
For example, if one of the author's of post comment changes its name, following will happen:
|
79
|
+
|
80
|
+
* Post fragment starts regenerating
|
81
|
+
* Author fragment is read from cache
|
82
|
+
* All comments (but the one with changed author) is read from cache
|
83
|
+
* The comment with changed author re-renders
|
84
|
+
* Dependencies are aggregated again and re-applied
|
85
|
+
* Post gets cached
|
86
|
+
|
87
|
+
`Cachex` stores dependencies in `REDIS` in both directions and it uses sets to manage them, so it works really fast.
|
88
|
+
|
89
|
+
|
90
|
+
## Installation
|
91
|
+
|
92
|
+
Add this line to your application's Gemfile:
|
93
|
+
|
94
|
+
gem 'cachex'
|
95
|
+
|
96
|
+
And then execute:
|
97
|
+
|
98
|
+
$ rails generate cachex:install
|
99
|
+
|
100
|
+
|
101
|
+
## Author
|
102
|
+
|
103
|
+
Developed by Milovan Zogovic.
|
data/Rakefile
ADDED
data/cachex.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/cachex/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Milovan Zogovic"]
|
6
|
+
gem.email = ["milovan.zogovic@gmail.com"]
|
7
|
+
gem.description = %q{Automated tag based fragment caching}
|
8
|
+
gem.summary = %q{Automated tag based fragment caching}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "cachex"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Cachex::VERSION
|
17
|
+
end
|
data/lib/cachex.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'cachex/adapters/redis_store'
|
2
|
+
require 'cachex/config'
|
3
|
+
require 'cachex/node'
|
4
|
+
require 'cachex/version'
|
5
|
+
require 'cachex/view_helpers'
|
6
|
+
require 'cachex/railtie'
|
7
|
+
|
8
|
+
module Cachex
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def cache(context, key, tags=[], &block)
|
12
|
+
if !@parent || @parent.key == 'root'
|
13
|
+
@parent = Node.new('root') # avoiding to reuse same root
|
14
|
+
end
|
15
|
+
|
16
|
+
node = Node.new(key, tags)
|
17
|
+
@parent.add_child node
|
18
|
+
|
19
|
+
if !Rails.cache.exist?(key) || !adapter.redis.exists(fqkey(key))
|
20
|
+
grandparent = @parent
|
21
|
+
@parent = node
|
22
|
+
content = context.capture(&block)
|
23
|
+
Rails.cache.write(key, content)
|
24
|
+
@parent = grandparent
|
25
|
+
|
26
|
+
# two-way dependency binding
|
27
|
+
adapter.redis.del fqkey(key)
|
28
|
+
if (all_tags = node.all_tags).length > 0
|
29
|
+
adapter.redis.sadd fqkey(key), all_tags
|
30
|
+
all_tags.each do |tag|
|
31
|
+
adapter.redis.sadd fqtag(tag), key
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
else
|
36
|
+
content = Rails.cache.read(key)
|
37
|
+
node.add_tags adapter.redis.smembers(fqkey(key))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Rails.logger.info "--- #{key} depends on #{node.all_tags.inspect}"
|
41
|
+
|
42
|
+
content
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def expire(*tags)
|
47
|
+
tags.each do |tag|
|
48
|
+
Rails.cache.delete tag
|
49
|
+
|
50
|
+
adapter.redis.smembers(fqtag(tag)).each do |key|
|
51
|
+
Rails.cache.delete key
|
52
|
+
end
|
53
|
+
adapter.redis.del fqtag(tag)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def fqkey(key)
|
59
|
+
"cachex_key_dependencies_#{key}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def fqtag(tag)
|
63
|
+
"cachex_tag_dependencies_#{tag}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def adapter
|
67
|
+
config.adapter
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Cachex
|
2
|
+
|
3
|
+
def self.config
|
4
|
+
if !@config
|
5
|
+
@config = Config.new
|
6
|
+
yield @config
|
7
|
+
end
|
8
|
+
@config
|
9
|
+
end
|
10
|
+
|
11
|
+
class Config
|
12
|
+
attr_reader :adapter
|
13
|
+
|
14
|
+
def adapter=(adapter)
|
15
|
+
case adapter.to_sym
|
16
|
+
when :redis
|
17
|
+
@adapter = Adapters::RedisStore.new
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Specified Cachex adapter is not supported!"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/cachex/node.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Cachex
|
2
|
+
|
3
|
+
class Node
|
4
|
+
attr_reader :key
|
5
|
+
|
6
|
+
def initialize(key, tags=[])
|
7
|
+
@children = []
|
8
|
+
@key = key
|
9
|
+
@tags = tags
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_child(node)
|
13
|
+
@children ||= []
|
14
|
+
@children << node
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_tags(additional_tags)
|
18
|
+
@tags += additional_tags
|
19
|
+
end
|
20
|
+
|
21
|
+
def all_tags
|
22
|
+
[@tags, @children.map(&:key), @children.map(&:all_tags)].flatten.compact.uniq
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Cachex
|
2
|
+
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("../../templates", __FILE__)
|
5
|
+
|
6
|
+
desc "Copies initializer script"
|
7
|
+
def copy_initializer
|
8
|
+
copy_file "cachex.rb", "config/initializers/cachex.rb"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cachex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Milovan Zogovic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-15 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Automated tag based fragment caching
|
15
|
+
email:
|
16
|
+
- milovan.zogovic@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- cachex.gemspec
|
27
|
+
- lib/cachex.rb
|
28
|
+
- lib/cachex/adapters/redis_store.rb
|
29
|
+
- lib/cachex/config.rb
|
30
|
+
- lib/cachex/node.rb
|
31
|
+
- lib/cachex/railtie.rb
|
32
|
+
- lib/cachex/version.rb
|
33
|
+
- lib/cachex/view_helpers.rb
|
34
|
+
- lib/generators/cachex/install_generator.rb
|
35
|
+
- lib/generators/templates/cachex.rb
|
36
|
+
homepage: ''
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.11
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Automated tag based fragment caching
|
60
|
+
test_files: []
|