primer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/README.rdoc +213 -0
  2. data/example/README.rdoc +69 -0
  3. data/example/application.rb +26 -0
  4. data/example/config.ru +5 -0
  5. data/example/environment.rb +31 -0
  6. data/example/models/blog_post.rb +4 -0
  7. data/example/models/connection.rb +10 -0
  8. data/example/public/style.css +75 -0
  9. data/example/script/setup_database.rb +11 -0
  10. data/example/views/index.erb +13 -0
  11. data/example/views/layout.erb +26 -0
  12. data/example/views/show.erb +7 -0
  13. data/example/worker.rb +3 -0
  14. data/lib/javascript/primer.js +36 -0
  15. data/lib/primer/bus/amqp.rb +43 -0
  16. data/lib/primer/bus/memory.rb +12 -0
  17. data/lib/primer/bus.rb +30 -0
  18. data/lib/primer/cache/memory.rb +60 -0
  19. data/lib/primer/cache/redis.rb +70 -0
  20. data/lib/primer/cache.rb +84 -0
  21. data/lib/primer/enabler.rb +18 -0
  22. data/lib/primer/helpers.rb +66 -0
  23. data/lib/primer/real_time.rb +80 -0
  24. data/lib/primer/route_set.rb +50 -0
  25. data/lib/primer/watcher/active_record_macros.rb +70 -0
  26. data/lib/primer/watcher/macros.rb +70 -0
  27. data/lib/primer/watcher.rb +62 -0
  28. data/lib/primer/worker/active_record_agent.rb +120 -0
  29. data/lib/primer/worker.rb +34 -0
  30. data/lib/primer.rb +31 -0
  31. data/spec/models/artist.rb +10 -0
  32. data/spec/models/blog_post.rb +5 -0
  33. data/spec/models/calendar.rb +7 -0
  34. data/spec/models/concert.rb +6 -0
  35. data/spec/models/performance.rb +6 -0
  36. data/spec/models/person.rb +14 -0
  37. data/spec/models/watchable.rb +17 -0
  38. data/spec/primer/bus_spec.rb +31 -0
  39. data/spec/primer/cache_spec.rb +309 -0
  40. data/spec/primer/helpers/erb_spec.rb +89 -0
  41. data/spec/primer/watcher/active_record_spec.rb +189 -0
  42. data/spec/primer/watcher_spec.rb +101 -0
  43. data/spec/schema.rb +31 -0
  44. data/spec/spec_helper.rb +60 -0
  45. data/spec/templates/page.erb +3 -0
  46. metadata +235 -0
data/README.rdoc ADDED
@@ -0,0 +1,213 @@
1
+ = Primer
2
+
3
+ This is an experiment to bring automatic cache expiry and regeneration to Rails.
4
+ At Songkick[http://www.songkick.com], we have a ton of code that deals with caches
5
+ and denormalization and messages and offline processing and it's hard to maintain.
6
+ I want to get rid of it. All of it.
7
+
8
+
9
+ == What?
10
+
11
+ Inspired by LunaScript[http://asana.com/luna] and Fun[http://marcuswest.in/read/fun-intro/],
12
+ I figured Ruby could figure out which values a computation uses, and use that to
13
+ expire caches for you without having to write any expiry code. This turns out to be
14
+ possible, at least for typical ActiveRecord usage, and Primer includes an engine
15
+ for that.
16
+
17
+ Primer currently lets you do the following:
18
+
19
+ * Mark up ERB templates with cache keys on Rails and Sinatra
20
+ * Monitor what attributes a cache value depends on
21
+ * Automatically expire a cache when its dependencies change
22
+ * Declare how caches should be recalculated for eager cache population
23
+ * Update pages in real time when their data is updated
24
+
25
+ It does all this without you having to write a single cache sweeper. You
26
+ just declare how to render your site, Primer does the rest.
27
+
28
+
29
+ == Enough waffle, show me the code!
30
+
31
+ The following is the minimal, 'hello world' use case. Get your ActiveRecord
32
+ model, put a mixn in it (<em>after</em> declaring the model's associations):
33
+
34
+ class BlogPost < ActiveRecord::Base
35
+ include Primer::Watcher
36
+ end
37
+
38
+ Set up a cache for your app (you need Redis[http://code.google.com/p/redis/]):
39
+
40
+ Primer.cache = Primer::Cache::Redis.new(:host => "10.0.1.1", :port => 6380)
41
+
42
+ Throw a helper in your views:
43
+
44
+ # Rails
45
+ module ApplicationHelper
46
+ include Primer::Helpers::ERB
47
+ end
48
+
49
+ # Sinatra
50
+ helpers { include Primer::Helpers::ERB }
51
+
52
+ Wrap cache blocks around your markup for expensive bits:
53
+
54
+ # views/posts/show.html.erb
55
+
56
+ <% primer "/posts/#{@post.id}/title" do %>
57
+ <%= @post.title.upcase %>
58
+ <% end %>
59
+
60
+ The output of the block gets cached to Redis using the given key. Once the
61
+ output is cached, the block will not be called again. The cache is invalidated
62
+ when (and only when) the title of <tt>@post</tt> changes; Primer figures this
63
+ out and you don't need to write any cache sweeping code.
64
+
65
+ Finally you need to bind the cache to the event bus, unless you want to run the
66
+ cache monitoring work in a background process (see below):
67
+
68
+ Primer.cache.bind_to_bus
69
+
70
+ # If you're using ActiveRecord
71
+ Primer::Worker::ActiveRecordAgent.bind_to_bus
72
+
73
+
74
+ === Declaring cache generators
75
+
76
+ You may have noticed that Primer forces the use of path-style keys for your
77
+ cache. Instead of wrapping code you want to memoize in a block, you can
78
+ declare how to calculate it elsewhere and use a router to map cache keys
79
+ to calculations. For example we could rewrite our post title example like
80
+ this:
81
+
82
+ # views/posts/show.html.erb
83
+ # note '=' sign here, not used with block form
84
+
85
+ <%= primer "/posts/#{@post.id}/title" %>
86
+
87
+ Then you can declare how to calculate this in a router attached to your
88
+ cache object:
89
+
90
+ Primer.cache.routes do
91
+ get "/posts/:id/title" do
92
+ post = BlogPost.find(params[:id])
93
+ post.title.upcase
94
+ end
95
+ end
96
+
97
+ The advantage of this is that the cache now has a way to generate cache values
98
+ outside of your rendering stack, meaning that instead of just invalidating the
99
+ cache it can actually calculate the new value so the cache is always ready
100
+ for incoming requests.
101
+
102
+ It also means you can generate cache content offline; running the following
103
+ will generate the cache of the first post's title:
104
+
105
+ Primer.cache.compute("/posts/1/title")
106
+
107
+
108
+ === Throttling
109
+
110
+ Let's say you have a cache value that depends on many little bits of model
111
+ data, a common situation in web front-ends. For example:
112
+
113
+ Primer.cache.routes do
114
+ get "/posts/:id/summary" do
115
+ post = BlogPost.find(params[:id])
116
+ <<-HTML
117
+ <div class="post-summary">
118
+ <h2>#{ post.title }</h2>
119
+ <ul class="post-meta">
120
+ <li>Posted #{ post.strftime('%A %e %B %Y') } by #{ post.author.name }</li>
121
+ <li>Tagged with #{ post.tags.map { |t| link_to(t.name, t) }.join(', ') }</li>
122
+ </ul>
123
+ </div>
124
+ HTML
125
+ end
126
+ end
127
+
128
+ We've got a few domain objects in use here: the post itself, its author, the
129
+ tags attached to the post. We'd want this value regenerating whenever any of
130
+ this data changes, but what if many values that affect this template change at
131
+ around the same time? We might not want to regenerate it for every single change,
132
+ we just want to make sure it looks okay after all the changes have been applied.
133
+ Primer lets you throttle cache regeneration, for example this makes sure each
134
+ key is never regenerated twice within a 5-second interval:
135
+
136
+ Primer.cache.throttle = 5
137
+
138
+ When a value affecting a key changes, Primer will wait 5 seconds before
139
+ regenerating it, allowing other data changes to accrue before we update the
140
+ cache.
141
+
142
+
143
+ === Background workers
144
+
145
+ You'll probably want to move a lot of the work Primer does out of your front-end
146
+ process. Primer includes an AMQP message bus to support this, and setting it up
147
+ is easy - put this somewhere in your app's setup:
148
+
149
+ Primer.bus = Primer::Bus::AMQP.new(:queue => 'my_app_events')
150
+
151
+ To make a background worker, you just need a file like this:
152
+
153
+ # worker.rb
154
+
155
+ # load your models, config, Primer routes etc
156
+ require 'path/to/app/environment'
157
+
158
+ Primer.worker!
159
+
160
+ Running <tt>ruby worker.rb</tt> will start a process in the shell that listens
161
+ for change notifications and updates the cache for you. You can start as many
162
+ of these as you like to spread the load out.
163
+
164
+
165
+ === Real-time page updates
166
+
167
+ If you want to be properly web-scale, you'll need to be updating your pages
168
+ in real time as your data changes. Primer lets you update any fragment generated
169
+ by a block-less <tt>primer</tt> call in your view automatically.
170
+
171
+ All you need to do is place some middleware in your Rack config:
172
+
173
+ # config.ru
174
+
175
+ require 'path/to/sinatra/app'
176
+
177
+ use Primer::RealTime
178
+ run Sinatra::Application
179
+
180
+ Add the client-side script to your templates (this must be in the <tt>HEAD</tt>):
181
+
182
+ <script type="text/javascript" src="/primer.js"></script>
183
+
184
+ Then configure it wherever your data model gets used to tell it you want to
185
+ use real-time updates and where the messaging server is. You should also set
186
+ a password - this will stop third parties being able to publish to the message
187
+ bus and inject arbitrary HTML into your pages.
188
+
189
+ Primer.real_time = true
190
+ Primer::RealTime.bayeux_server = 'http://localhost:9292'
191
+ Primer::RealTime.password = 'super_secret_password'
192
+
193
+
194
+ == Examples
195
+
196
+ See <tt>example/README.rdoc</tt>, a little Sinatra blog with a Redis cache,
197
+ offline cache workers and real-time page updates.
198
+
199
+
200
+ == Anything else?
201
+
202
+ I've tested it on Ruby 1.8.7 and 1.9.2 with Rails 3 and Sinatra 1.1. I've
203
+ briefly tried using it in Rails 2.2 and it looked okay-ish.
204
+
205
+ I'm NOT using this in production, and neither should you. Ideas and feedback
206
+ welcome, pull requests considered, bug reports likely to gather dust.
207
+
208
+
209
+ == License
210
+
211
+ Copyright (c) 2010 Songkick.com, James Coglan. Named by the inimitable
212
+ grillpanda[http://github.com/grillpanda]. Released under the MIT license.
213
+
@@ -0,0 +1,69 @@
1
+ = Primer example app
2
+
3
+ This is a simple app that demonstrates how you can use Primer. It's based on
4
+ ActiveRecord for the model and Sinatra for the front-end. To start it up,
5
+ install some gems, build the database then use Rack:
6
+
7
+ gem install redis amqp faye sinatra activerecord sqlite3-ruby
8
+ ruby script/setup_database.rb
9
+ rackup -s thin -E production config.ru
10
+
11
+
12
+ === models
13
+
14
+ Let's take a look at the internals. In the +models+ directory, you'll see a
15
+ simple BlogPost model:
16
+
17
+ class BlogPost < ActiveRecord::Base
18
+ include Primer::Watcher
19
+ end
20
+
21
+ That's all we need for Primer to monitor how our data changes.
22
+
23
+
24
+ === public, views
25
+
26
+ The +public+ and +views+ directories are used by the Sinatra app, and should
27
+ be pretty self-explanatory. Note which bits of the views are cached, and which
28
+ parts don't use blocks - we set these keys up in <tt>environment.rb</tt>.
29
+
30
+
31
+ === application.rb
32
+
33
+ The <tt>application.rb</tt> file is a simple Sinatra app - it loads the
34
+ model from <tt>environment.rb</tt> and just sets up a couple of pages.
35
+
36
+
37
+ === environment.rb
38
+
39
+ The meat of the system is in <tt>environment.rb</tt>. This file loads the
40
+ gems we need, loads our models, and configures Primer. See that file for more
41
+ details, but note that it uses an AMQP bus - this means we can compute cache
42
+ changes outside the app process. You'll need to start <tt>worker.rb</tt> so
43
+ that your changes get processed.
44
+
45
+
46
+ === worker.rb
47
+
48
+ This file just loads <tt>environment.rb</tt>, then tells Primer to start a
49
+ worker. The call to <tt>Primer.worker!</tt> is blocking, and makes Primer
50
+ pick up messages from the AMQP bus and process them. You can start as many
51
+ of these as you like to distribute the work. Just run:
52
+
53
+ ruby worker.rb
54
+
55
+ If you don't run any of these, messages will build up in AMQP until you start
56
+ a worker. Each data change message goes to only one worker process so you
57
+ won't duplicate any work.
58
+
59
+
60
+ === console
61
+
62
+ You can interact with the model by starting IRB with the app's environment:
63
+
64
+ irb -r ./environment.rb
65
+
66
+ Go and create posts and change their data to see how the front-end reacts.
67
+ To get the real-time updates to work you must have a worker running to
68
+ update caches; this is not done by the web app process.
69
+
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ class Application < Sinatra::Base
5
+ ROOT = File.expand_path(File.dirname(__FILE__))
6
+
7
+ require ROOT + '/environment'
8
+
9
+ set :reload_templates, true
10
+ set :static, true
11
+ set :public, ROOT + '/public'
12
+ set :views, ROOT + '/views'
13
+
14
+ helpers { include Primer::Helpers::ERB }
15
+
16
+ get '/' do
17
+ @posts = BlogPost.all
18
+ erb :index
19
+ end
20
+
21
+ get '/posts/:id' do
22
+ @post = BlogPost.find(params[:id])
23
+ erb :show
24
+ end
25
+ end
26
+
data/example/config.ru ADDED
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/application'
2
+
3
+ use Primer::RealTime
4
+ run Application
5
+
@@ -0,0 +1,31 @@
1
+ dir = File.expand_path(File.dirname(__FILE__))
2
+
3
+ # Load gems: ActiveRecord and Primer
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ require dir + '/../lib/primer'
7
+
8
+ # Load database config and models
9
+ require dir + '/models/connection'
10
+ require dir + '/models/blog_post'
11
+
12
+ # Configure Primer with a Redis cache and AMQP bus
13
+ Primer.cache = Primer::Cache::Redis.new
14
+ Primer.bus = Primer::Bus::AMQP.new(:queue => 'blog_changes')
15
+
16
+ # Enable real-time page updates
17
+ Primer.real_time = true
18
+ Primer::RealTime.bayeux_server = 'http://0.0.0.0:9292'
19
+ Primer::RealTime.password = 'omg_rofl_scale'
20
+
21
+ # Set up cache generation routes
22
+ Primer.cache.routes do
23
+ get '/posts/:id/date' do
24
+ BlogPost.find(params[:id]).created_at.strftime('%A %e %B %Y')
25
+ end
26
+
27
+ get '/posts/:id/title' do
28
+ BlogPost.find(params[:id]).title.upcase
29
+ end
30
+ end
31
+
@@ -0,0 +1,4 @@
1
+ class BlogPost < ActiveRecord::Base
2
+ include Primer::Watcher
3
+ end
4
+
@@ -0,0 +1,10 @@
1
+ require 'fileutils'
2
+ require 'rubygems'
3
+ require 'active_record'
4
+
5
+ dir = File.expand_path(File.dirname(__FILE__))
6
+
7
+ FileUtils.mkdir_p(dir + '/../db')
8
+ dbfile = dir + '/../db/blog.sqlite3'
9
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => dbfile)
10
+
@@ -0,0 +1,75 @@
1
+ body {
2
+ font: 16px/1.4 FreeSans, Helvetica, Arial, sans-serif;
3
+ background: #353e4b;
4
+ }
5
+
6
+ .sub {
7
+ width: 800px;
8
+ margin: 0 auto;
9
+ padding: 1em 2em;
10
+ }
11
+
12
+ .header {
13
+ text-shadow: #23282e 0px -2px 0px;
14
+ }
15
+
16
+ .header h1 {
17
+ color: #e5dec7;
18
+ font-size: 4em;
19
+ letter-spacing: -0.06em;
20
+ margin: 0;
21
+ }
22
+
23
+ .header h2 {
24
+ color: #b3a784;
25
+ font-size: 1.5em;
26
+ font-weight: normal;
27
+ letter-spacing: -0.06em;
28
+ margin: 0 0 0.5em;
29
+ }
30
+
31
+ .content .sub {
32
+ background: #fff;
33
+ color: #333;
34
+ -webkit-border-radius: 16px;
35
+ -moz-border-radius: 16px;
36
+ border-radius: 16px;
37
+ }
38
+
39
+ ul {
40
+ font-size: 1.5em;
41
+ list-style: none;
42
+ }
43
+
44
+ ul .date {
45
+ font-size: 0.8em;
46
+ letter-spacing: -0.06em;
47
+ color: #888;
48
+ }
49
+
50
+ h3 {
51
+ color: #9ba749;
52
+ font-size: 3em;
53
+ letter-spacing: -0.06em;
54
+ margin: 0;
55
+ }
56
+
57
+ h4 {
58
+ color: #888;
59
+ font-size: 1.5em;
60
+ font-weight: normal;
61
+ margin: 0 0 1em;
62
+ }
63
+
64
+ a {
65
+ color: #9ba749;
66
+ font-weight: bold;
67
+ text-decoration: none;
68
+ }
69
+
70
+ .footer {
71
+ font-size: 0.8em;
72
+ color: #999;
73
+ text-shadow: #23282e 0px -1px 0px;
74
+ }
75
+
@@ -0,0 +1,11 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/../models/connection'
2
+
3
+ ActiveRecord::Schema.define do |version|
4
+ create_table :blog_posts, :force => true do |t|
5
+ t.timestamps
6
+ t.string :title
7
+ t.text :body
8
+ t.string :author
9
+ end
10
+ end
11
+
@@ -0,0 +1,13 @@
1
+ <p>Look at all the awesome posts omg</p>
2
+
3
+ <ul>
4
+ <% @posts.each do |post| %>
5
+ <li>
6
+ <span class="date"><%= post.created_at.strftime '%d %b %Y' %></span>
7
+ <a href="/posts/<%= post.id %>">
8
+ <%= primer "/posts/#{post.id}/title", :span %>
9
+ </a>
10
+ </li>
11
+ <% end %>
12
+ </ul>
13
+
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>My ROFLscale blog</title>
6
+ <link rel="stylesheet" href="/style.css">
7
+ <script type="text/javascript" src="/primer.js"></script>
8
+ </head>
9
+ <body>
10
+
11
+ <div class="header"><div class="sub">
12
+ <h1>My ROFLscale blog</h1>
13
+ <h2>If you can&rsquo;t store it in <tt>/dev/null</tt> it isn&rsquo;t worth reading</h2>
14
+ </div></div>
15
+
16
+ <div class="content"><div class="sub">
17
+ <%= yield %>
18
+ </div></div>
19
+
20
+ <div class="footer"><div class="sub">
21
+ <p>Copyright &copy; 2010 That&rsquo;s What She Said</p>
22
+ </div></div>
23
+
24
+ </body>
25
+ </html>
26
+
@@ -0,0 +1,7 @@
1
+ <h3><%= @post.title.upcase %></h3>
2
+ <h4>By <%= @post.author %> &mdash; <%= primer "/posts/#{@post.id}/date", :span %></h4>
3
+
4
+ <% primer "/posts/#{@post.id}/body" do %>
5
+ <p><%= @post.body %></p>
6
+ <% end %>
7
+
data/example/worker.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/environment'
2
+ Primer.worker!
3
+
@@ -0,0 +1,36 @@
1
+ (function() {
2
+ window.PRIMER_CHANNELS = window.PRIMER_CHANNELS || [];
3
+
4
+ var BAYEUX_CLIENT = null;
5
+
6
+ var connect = function() {
7
+ BAYEUX_CLIENT = new Faye.Client('/primer/bayeux');
8
+ var i = PRIMER_CHANNELS.length;
9
+ while (i--) listen(PRIMER_CHANNELS.pop());
10
+ PRIMER_CHANNELS = {push: listen};
11
+ };
12
+
13
+ var listen = function(channel) {
14
+ BAYEUX_CLIENT.subscribe(channel, function(message) {
15
+ var node = document.getElementById(message.dom_id);
16
+ if (node) node.innerHTML = message.content;
17
+ });
18
+ };
19
+
20
+ var script = document.createElement('script'),
21
+ head = document.getElementsByTagName('head')[0];
22
+
23
+ script.type = 'text/javascript';
24
+ script.src = '/primer/bayeux.js';
25
+
26
+ script.onload = script.onreadystatechange = function() {
27
+ var state = script.readyState;
28
+ if (!state || state === 'loaded' || state === 'complete') {
29
+ script.onload = script.onreadystatechange = null;
30
+ head.removeChild(script);
31
+ connect();
32
+ }
33
+ };
34
+ head.appendChild(script);
35
+ })();
36
+
@@ -0,0 +1,43 @@
1
+ require 'mq'
2
+
3
+ module Primer
4
+ class Bus
5
+
6
+ class AMQP < Bus
7
+ def initialize(config = {})
8
+ @config = config
9
+ super()
10
+ end
11
+
12
+ def publish(topic, message)
13
+ tuple = [topic, message]
14
+ queue.publish(YAML.dump(tuple))
15
+ end
16
+
17
+ def subscribe(*args, &block)
18
+ bind
19
+ super
20
+ end
21
+
22
+ private
23
+
24
+ def queue
25
+ Faye.ensure_reactor_running!
26
+ return @queue if defined?(@queue)
27
+ raise "I need a queue name!" unless @config[:queue]
28
+ @queue = MQ.new.queue(@config[:queue])
29
+ end
30
+
31
+ def bind
32
+ return if @bound
33
+ queue.subscribe do |message|
34
+ data = YAML.load(message)
35
+ distribute(*data)
36
+ end
37
+ @bound = true
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+
@@ -0,0 +1,12 @@
1
+ module Primer
2
+ class Bus
3
+
4
+ class Memory < Bus
5
+ def publish(topic, message)
6
+ distribute(topic, message)
7
+ end
8
+ end
9
+
10
+ end
11
+ end
12
+
data/lib/primer/bus.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Primer
2
+ class Bus
3
+
4
+ autoload :Memory, ROOT + '/primer/bus/memory'
5
+ autoload :AMQP, ROOT + '/primer/bus/amqp'
6
+
7
+ def initialize
8
+ unsubscribe_all
9
+ end
10
+
11
+ def distribute(topic, message)
12
+ return unless @listeners.has_key?(topic)
13
+ @listeners[topic].each { |cb| cb.call(message) }
14
+ end
15
+
16
+ def subscribe(topic, &listener)
17
+ @listeners[topic].add(listener)
18
+ end
19
+
20
+ def unsubscribe(topic, &listener)
21
+ @listeners[topic].delete(listener)
22
+ end
23
+
24
+ def unsubscribe_all
25
+ @listeners = Hash.new { |h,k| h[k] = Set.new }
26
+ end
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,60 @@
1
+ module Primer
2
+ class Cache
3
+
4
+ class Memory < Cache
5
+ def initialize
6
+ clear
7
+ end
8
+
9
+ def clear
10
+ @data_store = {}
11
+ @relations = {}
12
+ @dependencies = {}
13
+ end
14
+
15
+ def put(cache_key, value)
16
+ validate_key(cache_key)
17
+ @data_store[cache_key] = value
18
+ publish_change(cache_key)
19
+ end
20
+
21
+ def get(cache_key)
22
+ validate_key(cache_key)
23
+ @data_store[cache_key]
24
+ end
25
+
26
+ def has_key?(cache_key)
27
+ @data_store.has_key?(cache_key)
28
+ end
29
+
30
+ def invalidate(cache_key)
31
+ @data_store.delete(cache_key)
32
+ return unless deps = @dependencies[cache_key]
33
+ deps.each do |attribute|
34
+ @relations[attribute].delete(cache_key)
35
+ end
36
+ @dependencies.delete(cache_key)
37
+ end
38
+
39
+ def relate(cache_key, attributes)
40
+ deps = @dependencies[cache_key] ||= Set.new
41
+ attributes.each do |attribute|
42
+ deps.add(attribute)
43
+ list = @relations[attribute] ||= Set.new
44
+ list.add(cache_key)
45
+ end
46
+ end
47
+
48
+ def keys_for_attribute(attribute)
49
+ return [] unless keys = @relations[attribute]
50
+ keys.to_a
51
+ end
52
+
53
+ def timeout(cache_key, &block)
54
+ add_timeout(cache_key, @throttle, &block)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+