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.
- data/README.rdoc +213 -0
- data/example/README.rdoc +69 -0
- data/example/application.rb +26 -0
- data/example/config.ru +5 -0
- data/example/environment.rb +31 -0
- data/example/models/blog_post.rb +4 -0
- data/example/models/connection.rb +10 -0
- data/example/public/style.css +75 -0
- data/example/script/setup_database.rb +11 -0
- data/example/views/index.erb +13 -0
- data/example/views/layout.erb +26 -0
- data/example/views/show.erb +7 -0
- data/example/worker.rb +3 -0
- data/lib/javascript/primer.js +36 -0
- data/lib/primer/bus/amqp.rb +43 -0
- data/lib/primer/bus/memory.rb +12 -0
- data/lib/primer/bus.rb +30 -0
- data/lib/primer/cache/memory.rb +60 -0
- data/lib/primer/cache/redis.rb +70 -0
- data/lib/primer/cache.rb +84 -0
- data/lib/primer/enabler.rb +18 -0
- data/lib/primer/helpers.rb +66 -0
- data/lib/primer/real_time.rb +80 -0
- data/lib/primer/route_set.rb +50 -0
- data/lib/primer/watcher/active_record_macros.rb +70 -0
- data/lib/primer/watcher/macros.rb +70 -0
- data/lib/primer/watcher.rb +62 -0
- data/lib/primer/worker/active_record_agent.rb +120 -0
- data/lib/primer/worker.rb +34 -0
- data/lib/primer.rb +31 -0
- data/spec/models/artist.rb +10 -0
- data/spec/models/blog_post.rb +5 -0
- data/spec/models/calendar.rb +7 -0
- data/spec/models/concert.rb +6 -0
- data/spec/models/performance.rb +6 -0
- data/spec/models/person.rb +14 -0
- data/spec/models/watchable.rb +17 -0
- data/spec/primer/bus_spec.rb +31 -0
- data/spec/primer/cache_spec.rb +309 -0
- data/spec/primer/helpers/erb_spec.rb +89 -0
- data/spec/primer/watcher/active_record_spec.rb +189 -0
- data/spec/primer/watcher_spec.rb +101 -0
- data/spec/schema.rb +31 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/templates/page.erb +3 -0
- 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,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
|
+
|