primer 0.1.0

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.
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
+