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
@@ -0,0 +1,34 @@
1
+ module Primer
2
+ class Worker
3
+
4
+ class ConfigError < StandardError ; end
5
+
6
+ autoload :ActiveRecordAgent, ROOT + '/primer/worker/active_record_agent'
7
+
8
+ def run
9
+ raise ConfigError.new('No cache present') unless Primer.cache
10
+ raise ConfigError.new('No message bus present') unless Primer.bus
11
+
12
+ puts "Cache: #{ Primer.cache }"
13
+ puts "Message bus: #{ Primer.bus }"
14
+ puts
15
+
16
+ EM.run {
17
+ Primer.bus.subscribe :active_record do |args|
18
+ puts "[active_record] #{ args.inspect }"
19
+ end
20
+ Primer.bus.subscribe :changes do |args|
21
+ puts "[changes] #{ args.inspect }"
22
+ end
23
+
24
+ ActiveRecordAgent.bind_to_bus
25
+ Primer.cache.bind_to_bus
26
+
27
+ puts "Listening for messages..."
28
+ puts
29
+ }
30
+ end
31
+
32
+ end
33
+ end
34
+
data/lib/primer.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'faye'
2
+ require 'set'
3
+ require 'yaml'
4
+
5
+ module Primer
6
+ ROOT = File.expand_path(File.dirname(__FILE__))
7
+ VERSION = '0.1.0'
8
+
9
+ class InvalidKey < StandardError ; end
10
+ class RouteNotFound < StandardError ; end
11
+
12
+ autoload :Cache, ROOT + '/primer/cache'
13
+ autoload :Bus, ROOT + '/primer/bus'
14
+ autoload :RouteSet, ROOT + '/primer/route_set'
15
+ autoload :Enabler, ROOT + '/primer/enabler'
16
+ autoload :Watcher, ROOT + '/primer/watcher'
17
+ autoload :Helpers, ROOT + '/primer/helpers'
18
+ autoload :RealTime, ROOT + '/primer/real_time'
19
+ autoload :Worker, ROOT + '/primer/worker'
20
+
21
+ class << self
22
+ attr_accessor :cache, :bus, :real_time
23
+ end
24
+
25
+ self.bus = Bus::Memory.new
26
+
27
+ def self.worker!
28
+ Worker.new.run
29
+ end
30
+ end
31
+
@@ -0,0 +1,10 @@
1
+ class Artist < ActiveRecord::Base
2
+ has_many :performances
3
+ has_many :concerts, :through => :performances
4
+
5
+ belongs_to :calendar
6
+ has_many :upcoming_gigs, :through => :calendar, :source => :gigs
7
+
8
+ include Primer::Watcher
9
+ end
10
+
@@ -0,0 +1,5 @@
1
+ class BlogPost < ActiveRecord::Base
2
+ belongs_to :person
3
+ include Primer::Watcher
4
+ end
5
+
@@ -0,0 +1,7 @@
1
+ class Calendar < ActiveRecord::Base
2
+ has_many :artists
3
+ has_many :gigs, :class_name => 'Concert'
4
+
5
+ include Primer::Watcher
6
+ end
7
+
@@ -0,0 +1,6 @@
1
+ class Concert < ActiveRecord::Base
2
+ belongs_to :calendar
3
+ has_many :performances, :dependent => :destroy
4
+ include Primer::Watcher
5
+ end
6
+
@@ -0,0 +1,6 @@
1
+ class Performance < ActiveRecord::Base
2
+ belongs_to :artist
3
+ belongs_to :concert
4
+ include Primer::Watcher
5
+ end
6
+
@@ -0,0 +1,14 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :blog_posts
3
+
4
+ include Primer::Watcher
5
+
6
+ def all_attributes
7
+ [id, the_name]
8
+ end
9
+
10
+ def the_name
11
+ name
12
+ end
13
+ end
14
+
@@ -0,0 +1,17 @@
1
+ class Watchable
2
+ include Primer::Watcher
3
+ watch_calls_to :name, :is_called?
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def name
10
+ @name
11
+ end
12
+
13
+ def is_called?(name)
14
+ @name == name
15
+ end
16
+ end
17
+
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "primer event bus" do
4
+ before do
5
+ @message = nil
6
+ bus.subscribe(:changes) { |message| @message = message }
7
+ end
8
+
9
+ it "transmits messages verbatim" do
10
+ bus.publish :changes, ["series", "of", "params"]
11
+ sleep 1.0
12
+ @message.should == ["series", "of", "params"]
13
+ end
14
+
15
+ it "routes messages to the right channel" do
16
+ bus.publish :other, "something"
17
+ sleep 1.0
18
+ @message.should be_nil
19
+ end
20
+ end
21
+
22
+ describe Primer::Bus::Memory do
23
+ let(:bus) { Primer::Bus::Memory.new }
24
+ it_should_behave_like "primer event bus"
25
+ end
26
+
27
+ describe Primer::Bus::AMQP do
28
+ let(:bus) { Primer::Bus::AMQP.new(:queue => 'data_changes') }
29
+ it_should_behave_like "primer event bus"
30
+ end
31
+
@@ -0,0 +1,309 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "primer cache" do
4
+ before do
5
+ Primer.cache = cache
6
+ Primer.bus = bus
7
+
8
+ cache.bind_to_bus
9
+ Primer::Worker::ActiveRecordAgent.bind_to_bus
10
+ sync
11
+
12
+ @person = Person.create(:name => "Abe")
13
+ @impostor = Person.create(:name => "Aaron")
14
+ @post = BlogPost.create(:person => @impostor, :title => "roflmillions")
15
+ end
16
+
17
+ def sync
18
+ sleep sync_time
19
+ end
20
+
21
+ describe "#compute with a block" do
22
+ def compute_value
23
+ cache.compute("/people/abe/name") { @person.name }
24
+ end
25
+
26
+ it "returns the value of the block" do
27
+ compute_value.should == "Abe"
28
+ end
29
+
30
+ it "calls the implementation to get the value" do
31
+ @person.should_receive(:name)
32
+ compute_value
33
+ end
34
+
35
+ it "stores the result of the computation" do
36
+ cache.should_receive(:put).with("/people/abe/name", "Abe")
37
+ compute_value
38
+ end
39
+
40
+ it "notes that the value is related to some ActiveRecord data" do
41
+ cache.should_receive(:relate).with("/people/abe/name", [["ActiveRecord", "Person", @person.id, "name"]])
42
+ compute_value
43
+ end
44
+
45
+ describe "when the value is already known" do
46
+ before { compute_value }
47
+
48
+ it "returns the value of the block" do
49
+ compute_value.should == "Abe"
50
+ end
51
+
52
+ it "does not call the implementation" do
53
+ @person.should_not_receive(:name)
54
+ compute_value
55
+ end
56
+
57
+ it "invalidates the cache when related data changes" do
58
+ cache.should_receive(:invalidate).with("/people/abe/name")
59
+ @person.update_attribute(:name, "Aaron")
60
+ sync
61
+ end
62
+
63
+ it "does not invalidate the cache when a different object changes" do
64
+ cache.should_not_receive(:invalidate)
65
+ @impostor.update_attribute(:name, "Weeble")
66
+ sync
67
+ end
68
+
69
+ it "does not invalidate the cache when unrelated data changes" do
70
+ cache.should_not_receive(:invalidate)
71
+ @person.update_attribute(:age, 28)
72
+ sync
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "#compute without a block" do
78
+ let(:compute_value) { cache.compute("/name") }
79
+ let(:compute_count) { cache.compute("/count") }
80
+ let(:compute_author) { cache.compute("/author") }
81
+
82
+ before do
83
+ cache.routes = Primer::RouteSet.new do
84
+ get('/name') { Person.first.name }
85
+ get('/bar/:id') { params[:id] }
86
+ get('/count') { Person.first.blog_posts.count }
87
+ get('/author') { BlogPost.first.person.name }
88
+ end
89
+ end
90
+
91
+ it "uses the routes to find the right value" do
92
+ compute_value.should == "Abe"
93
+ end
94
+
95
+ it "stores the result of the computation" do
96
+ cache.should_receive(:put).with("/name", "Abe")
97
+ compute_value
98
+ end
99
+
100
+ it "notes that the value is related to some ActiveRecord data" do
101
+ cache.should_receive(:relate).with("/name", [["ActiveRecord", "Person", @person.id, "name"]])
102
+ compute_value
103
+ end
104
+
105
+ it "handles routing patterns and params" do
106
+ cache.should_receive(:put).with("/bar/pattern_match", "pattern_match")
107
+ cache.should_receive(:relate).with("/bar/pattern_match", [])
108
+ cache.compute("/bar/pattern_match").should == "pattern_match"
109
+ end
110
+
111
+ it "raise an error for paths that don't match anything" do
112
+ cache.should_not_receive(:put)
113
+ cache.should_not_receive(:relate)
114
+ lambda { cache.compute("/qux") }.should raise_error(Primer::RouteNotFound)
115
+ end
116
+
117
+ describe "when the value is already known" do
118
+ before do
119
+ compute_value
120
+ compute_count
121
+ compute_author
122
+ end
123
+
124
+ it "returns the value of the block" do
125
+ compute_value.should == "Abe"
126
+ end
127
+
128
+ it "does not call the implementation" do
129
+ @person.should_not_receive(:name)
130
+ compute_value
131
+ end
132
+
133
+ it "regenerates the cache when related data changes" do
134
+ @person.update_attribute(:name, "Aaron")
135
+ sync
136
+ cache.get("/name").should == "Aaron"
137
+ end
138
+
139
+ it "regenerates the cache when an associated collection changes" do
140
+ BlogPost.create(:person => @person, :title => "ROFLscale")
141
+ sync
142
+ cache.get("/count").to_i.should == 1
143
+ @person.blog_posts.destroy_all
144
+ sync
145
+ cache.get("/count").to_i.should == 0
146
+ end
147
+
148
+ it "regenerates the cache when an association is changed" do
149
+ @post.update_attribute(:person, @person)
150
+ sync
151
+ cache.get("/author").should == "Abe"
152
+ end
153
+
154
+ it "regenerates the cache when an associated object changes" do
155
+ @impostor.update_attribute(:name, "Steve")
156
+ sync
157
+ cache.get("/author").should == "Steve"
158
+ end
159
+
160
+ it "removes a cache key when data it uses is deleted" do
161
+ cache.compute("/aaron") { Person.find_by_name("Aaron").name }
162
+ sync
163
+ cache.get("/aaron").should == "Aaron"
164
+ @impostor.destroy
165
+ sync
166
+ cache.get("/aaron").should be_nil
167
+ cache.has_key?("/aaron").should be_false
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "nested cache values" do
173
+ before do
174
+ cache.routes = Primer::RouteSet.new do
175
+ get('/title') { BlogPost.first.title }
176
+ get('/content') { BlogPost.first.person.name + Primer.cache.compute('/title') }
177
+ end
178
+ end
179
+
180
+ it "returns the correct value for the outer computation" do
181
+ cache.compute("/content").should == "Aaronroflmillions"
182
+ end
183
+
184
+ describe "when the inner cache value is known before computing the outer value" do
185
+ before do
186
+ cache.compute("/title")
187
+ cache.compute("/content")
188
+ end
189
+
190
+ it "returns the correct value for the outer computation" do
191
+ cache.compute("/content").should == "Aaronroflmillions"
192
+ end
193
+
194
+ it "updates the cache when the title changes" do
195
+ @post.update_attribute(:title, "It's toasted")
196
+ sync
197
+ cache.get("/title").should == "It's toasted"
198
+ cache.get("/content").should == "AaronIt's toasted"
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "throttling" do
204
+ before do
205
+ cache.routes = Primer::RouteSet.new do
206
+ get("/data") { [BlogPost.first.title, Person.first.name] }
207
+ end
208
+ cache.throttle = 0.5
209
+ cache.compute("/data")
210
+ end
211
+
212
+ it "restricts how often the cache is recalculated" do
213
+ cache.should_receive(:regenerate).once
214
+ @post.update_attribute(:title, "The new title")
215
+ @person.update_attribute(:name, "The new name")
216
+ sleep 1.0
217
+ end
218
+
219
+ it "regenerates after the given throttle time, not after the first trigger" do
220
+ @post.update_attribute(:title, "The new title")
221
+ cache.get("/data").should == ["roflmillions", "Abe"]
222
+
223
+ @person.update_attribute(:name, "The new name")
224
+ cache.get("/data").should == ["roflmillions", "Abe"]
225
+
226
+ sleep 1.0
227
+ cache.get("/data").should == ["The new title", "The new name"]
228
+ end
229
+ end
230
+
231
+ describe "#get" do
232
+ it "can be caught by the Watcher" do
233
+ calls = []
234
+ Primer::Watcher.watching(calls) { cache.get("/a/key") }
235
+ calls.should == [[cache, :get, ["/a/key"], nil, nil]]
236
+ end
237
+ end
238
+
239
+ describe "#put" do
240
+ before { cache.get("/key").should be_nil }
241
+
242
+ it "writes a value to the cache" do
243
+ cache.put("/key", "value")
244
+ cache.get("/key").should == "value"
245
+ end
246
+
247
+ it "raises an error if any invalid key is used" do
248
+ lambda { cache.put("invalid", "hmm") }.should raise_error(Primer::InvalidKey)
249
+ end
250
+
251
+ it "can store arbitrary data" do
252
+ value = ["foo", 4, [5, :bar], {:qux => [6, 7]}]
253
+ cache.put("/key", value)
254
+ cache.get("/key").should == value
255
+ end
256
+ end
257
+
258
+ describe "#invalidate" do
259
+ before { cache.put("/some/key", "value") }
260
+
261
+ it "removes the key from the cache" do
262
+ cache.invalidate("/some/key")
263
+ cache.get("/some/key").should be_nil
264
+ end
265
+
266
+ describe "when a cache value has been generated from a computation" do
267
+ before { cache.compute("/people/abe/name") { @person.name } }
268
+
269
+ it "removes existing relations between the model and the cache" do
270
+ cache.invalidate("/people/abe/name")
271
+ cache.should_not_receive(:invalidate)
272
+ @person.update_attribute(:name, "Weeble")
273
+ end
274
+ end
275
+ end
276
+
277
+ describe "#clear" do
278
+ before { cache.put("/some/key", "value") }
279
+
280
+ it "empties the cache" do
281
+ cache.clear
282
+ cache.get("/some/key").should be_nil
283
+ end
284
+ end
285
+ end
286
+
287
+ describe Primer::Cache::Memory do
288
+ let(:cache) { Primer::Cache::Memory.new }
289
+
290
+ describe "with an in-memory bus" do
291
+ let(:bus) { Primer::Bus::Memory.new }
292
+ let(:sync_time) { 0 }
293
+ it_should_behave_like "primer cache"
294
+ end
295
+
296
+ describe "with an AMQP bus" do
297
+ let(:bus) { Primer::Bus::AMQP.new(:queue => "data_changes_#{Helper.next_id}") }
298
+ let(:sync_time) { 0.2 }
299
+ it_should_behave_like "primer cache"
300
+ end
301
+ end
302
+
303
+ describe Primer::Cache::Redis do
304
+ let(:cache) { Primer::Cache::Redis.new }
305
+ let(:bus) { Primer::Bus::Memory.new }
306
+ let(:sync_time) { 0 }
307
+ it_should_behave_like "primer cache"
308
+ end
309
+
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ module RenderingHelper
4
+ include Primer::Helpers::ERB
5
+ attr_accessor :name
6
+ end
7
+
8
+ class Context
9
+ include RenderingHelper
10
+ end
11
+
12
+ shared_examples_for "erb helper" do
13
+ before { context.name = "Aaron" }
14
+
15
+ before do
16
+ Primer.cache = Primer::Cache::Memory.new
17
+ Primer.cache.routes = Primer::RouteSet.new do
18
+ get('/user') { "Master" }
19
+ end
20
+ end
21
+
22
+ describe "#primer" do
23
+ describe "with an empty cache" do
24
+ it "renders the contents of the block" do
25
+ output.should == "Welcome, Master\nHello, Aaron\nThanks\n"
26
+ end
27
+
28
+ it "uses the model to get data" do
29
+ context.should_receive(:name).and_return("Abe")
30
+ output.should == "Welcome, Master\nHello, Abe\nThanks\n"
31
+ end
32
+
33
+ it "writes the block result to the cache" do
34
+ Primer.cache.should_receive(:put).with("/user", "Master")
35
+ Primer.cache.should_receive(:put).with("/cache/key", "Hello, Aaron")
36
+ output
37
+ end
38
+ end
39
+
40
+ describe "with a value in the cache" do
41
+ before { Primer.cache.put("/cache/key", "Text from the cache") }
42
+
43
+ it "does not use the model to get data" do
44
+ context.should_not_receive(:name)
45
+ output
46
+ end
47
+
48
+ it "renders the cached value into the template" do
49
+ output.should == "Welcome, Master\nText from the cache\nThanks\n"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "Rails3 ERB templates" do
56
+ class ApplicationController < ActionController::Base
57
+ def self._helpers
58
+ RenderingHelper
59
+ end
60
+ end
61
+
62
+ before do
63
+ view_context = context
64
+ controller.stub(:view_context).and_return(view_context)
65
+ end
66
+
67
+ let(:controller) { ApplicationController.new }
68
+ let(:context) { controller.view_context }
69
+
70
+ let :output do
71
+ controller.render(:file => "spec/templates/page.erb", :action => "show") rescue
72
+ body = controller.response_body
73
+ Array === body ? body.first : body
74
+ end
75
+
76
+ it_should_behave_like "erb helper"
77
+ end
78
+
79
+ describe "Sinatra ERB templates" do
80
+ let(:context) { Context.new }
81
+
82
+ let :output do
83
+ template = Tilt[:erb].new("spec/templates/page.erb", 0, :outvar => "@_out_buf")
84
+ template.render(context)
85
+ end
86
+
87
+ it_should_behave_like "erb helper"
88
+ end
89
+