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.
@@ -0,0 +1,18 @@
1
+ *.DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cachex.gemspec
4
+ gemspec
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.
@@ -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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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
@@ -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,8 @@
1
+ module Cachex
2
+ module Adapters
3
+ class RedisStore
4
+ attr_accessor :redis
5
+
6
+ end
7
+ end
8
+ 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
@@ -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,7 @@
1
+ module Cachex
2
+ class Railtie < Rails::Railtie
3
+ initializer 'cachex.view_helpers' do
4
+ ActionView::Base.send :include, ViewHelpers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Cachex
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ module Cachex
2
+ module ViewHelpers
3
+ def cachex(key, *tags, &block)
4
+ Cachex.cache self, key, tags, &block
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,4 @@
1
+ Cachex.config do |c|
2
+ c.adapter = :redis
3
+ c.adapter.redis = Redis.new(host: '127.0.0.1', port: '6379')
4
+ 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: []