cache-machine 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +3 -1
  2. data/.travis.yml +6 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +0 -2
  5. data/README.md +258 -0
  6. data/VERSION +1 -1
  7. data/cache-machine.gemspec +7 -22
  8. data/db/.gitignore +1 -0
  9. data/lib/cache-machine.rb +6 -2
  10. data/lib/cache_machine/adapter.rb +127 -0
  11. data/lib/cache_machine/adapters/rails.rb +58 -0
  12. data/lib/cache_machine/cache.rb +34 -85
  13. data/lib/cache_machine/cache/associations.rb +29 -0
  14. data/lib/cache_machine/cache/class_timestamp.rb +31 -0
  15. data/lib/cache_machine/cache/collection.rb +131 -0
  16. data/lib/cache_machine/cache/map.rb +97 -241
  17. data/lib/cache_machine/cache/mapper.rb +135 -0
  18. data/lib/cache_machine/cache/resource.rb +108 -0
  19. data/lib/cache_machine/cache/scope.rb +24 -0
  20. data/lib/cache_machine/cache/timestamp_builder.rb +58 -0
  21. data/lib/cache_machine/helpers/cache_helper.rb +3 -3
  22. data/lib/cache_machine/logger.rb +9 -8
  23. data/lib/cache_machine/railtie.rb +11 -0
  24. data/lib/cache_machine/tasks.rb +10 -0
  25. data/spec/fixtures.rb +11 -6
  26. data/spec/lib/cache_machine/adapters/rails_spec.rb +149 -0
  27. data/spec/lib/cache_machine/cache/associations_spec.rb +32 -0
  28. data/spec/lib/cache_machine/cache/class_timestam_spec.rb +29 -0
  29. data/spec/lib/cache_machine/cache/collection_spec.rb +106 -0
  30. data/spec/lib/cache_machine/cache/map_spec.rb +34 -0
  31. data/spec/lib/cache_machine/cache/mapper_spec.rb +61 -0
  32. data/spec/lib/cache_machine/cache/resource_spec.rb +86 -0
  33. data/spec/lib/cache_machine/cache/scope_spec.rb +50 -0
  34. data/spec/lib/cache_machine/cache/timestamp_builder_spec.rb +52 -0
  35. data/spec/spec_helper.rb +9 -3
  36. metadata +35 -61
  37. data/README.rdoc +0 -186
  38. data/spec/lib/cache_machine_spec.rb +0 -161
data/.gitignore CHANGED
@@ -5,7 +5,7 @@
5
5
  .idea
6
6
 
7
7
  # test database
8
- db
8
+ db/*.db
9
9
 
10
10
  # rcov generated
11
11
  coverage
@@ -22,3 +22,5 @@ doc
22
22
 
23
23
  # jeweler generated
24
24
  pkg
25
+
26
+ .rspec
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ script: bundle exec rspec spec/lib/cache_machine/adapters/rails_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/associations_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/class_timestam_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/collection_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/map_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/mapper_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/resource_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/scope_spec.rb && bundle exec rspec spec/lib/cache_machine/cache/timestamp_builder_spec.rb
data/Gemfile CHANGED
@@ -1,8 +1,6 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gem "rails"
4
- gem "activemodel"
5
- gem "activesupport"
6
4
 
7
5
  group :development do
8
6
  gem "bundler", "~> 1.0.0"
@@ -93,8 +93,6 @@ PLATFORMS
93
93
  ruby
94
94
 
95
95
  DEPENDENCIES
96
- activemodel
97
- activesupport
98
96
  bundler (~> 1.0.0)
99
97
  jeweler (~> 1.6.2)
100
98
  rails
@@ -0,0 +1,258 @@
1
+ # ![cache-machine](http://img195.imageshack.us/img195/5371/cachemachinefinal2.png)
2
+
3
+ An ActiveRecord mixin that helps managing cached content in a Ruby on Rails application with complex data update dependencies.
4
+
5
+ Cache Machine provides:
6
+
7
+ - high-level methods for accessing cached content using page names, numbers, time stamps etc,
8
+ - a DSL to describe update dependencies between the data models underlying the cached content,
9
+ - automatic cache invalidation based on those explicitly modeled data update dependencies.
10
+
11
+ You will find Cache Machine useful if you:
12
+
13
+ - use Memcache to cache fragments of a web site that contain data from a variety of underlying data models
14
+ - anytime one of the underlying data models changes, all the cached page fragments in which this data model occurs - and only those - need to be invalidated/updated
15
+ - you have many data models, cached fragments, and many data models used inside each cached fragment
16
+ - you want to update cache from background job (i.e. cache-sweeper does not know about your changes)
17
+
18
+ Cache Machine is library agnostic. You can use your own cache adapters (see below).
19
+
20
+ # Usage
21
+
22
+ Setup your cache dependencies in config/initializers/cache-machine.rb using <b>cache map</b>. Very similar to Rails routes:
23
+
24
+ ```ruby
25
+ CacheMachine::Cache::Map.new.draw do
26
+ resource City do
27
+ collection :streets do
28
+ member :houses
29
+ end
30
+
31
+ collection :houses do
32
+ member :bricks
33
+ member :windows
34
+ end
35
+ end
36
+
37
+ resource Street do
38
+ collection :houses
39
+ collection :walls
40
+ end
41
+
42
+ resource House do
43
+ collection :walls, :scope => :vertical, :timestamp => false do
44
+ members :front_walls, :side_walls
45
+ member :bricks
46
+ member :windows
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ In this case your models should look like this:
53
+
54
+ ```ruby
55
+ class City < ActiveRecord::Base
56
+ has_many :streets
57
+ has_many :houses, :through => :streets
58
+ end
59
+
60
+ class Street < ActiveRecord::Base
61
+ belongs_to :city
62
+ has_many :houses
63
+ has_many :walls, :through => :houses
64
+ end
65
+
66
+ class House < ActiveRecord::Base
67
+ belongs_to :street
68
+ has_many :walls
69
+ end
70
+
71
+ class Wall < ActiveRecord::Base
72
+ belongs_to :house
73
+ # has_many :bricks
74
+ end
75
+ ```
76
+
77
+ This example shows you how changes in your database affect on cache:
78
+
79
+ - When you create/update/destroy any <b>wall</b>:
80
+ - cache of <b>walls collection</b> expired for <b>house</b> associated with that updated/created/destroyed wall
81
+ - cache of <b>walls collection</b> expired for <b>street</b> (where wall's house is located) associated with that updated/created/destroyed
82
+ - cache of <b>front_walls</b> and <b>side_walls</b> expired for <b>house</b> associated with that updated/created/destroyed wall
83
+ - cache of <b>bricks</b> expired for <b>house</b> associated with that updated/created/destroyed wall
84
+ - cache of <b>windows</b> expired for <b>house</b> associated with that updated/created/destroyed wall
85
+ - When you create/update/destroy any <b>house</b>:
86
+ - cache of <b>houses</b> updated for associated <b>street</b>
87
+ - cache of <b>houses</b> updated for associated <b>city</b>
88
+ - When you create/update/destroy any <b>street</b>:
89
+ - cache of <b>streets</b> updated for associated <b>city</b>
90
+ - cache of <b>houses</b> updated for associated <b>city</b>
91
+ - ... :)
92
+
93
+ <b>Member may have any name, whatever you want. But invalidation process starts only when collection is changed.</b>
94
+
95
+ ## Custom cache invalidation
96
+
97
+ ### Using timestamps
98
+ Timestamps allow you to build very complex and custom cache dependencies.
99
+
100
+ In your model:
101
+
102
+ ```ruby
103
+ class House < ActiveRecord::Base
104
+ define_timestamp(:walls_timestamp) { [ bricks.count, windows.last.updated_at ] }
105
+ end
106
+ ```
107
+
108
+ Anywhere else:
109
+
110
+ ```ruby
111
+ @house.fetch_cache_of :walls, :timestamp => :walls_timestamp do
112
+ walls.where(:built_at => Date.today)
113
+ end
114
+ ```
115
+
116
+ This way you add additional condition to cache-key used for fetching data from cache:
117
+ Any time when bricks count is changed or any window is updated your cache key will be changed and block will return fresh data.
118
+ Timestamp should return array or string.
119
+
120
+ ### Using Cache Machine timestamps
121
+ Suppose you need to reset cache of _tweets_ every 10 minutes.
122
+
123
+ ```ruby
124
+ class LadyGaga < ActiveRecord::Base
125
+ define_timestamp :tweets_timestamp, :expires_in => 10.minutes do
126
+ ...
127
+ end
128
+ end
129
+
130
+ #...
131
+
132
+ # Somewhere
133
+ @lady_gaga.fetch_cache_of :tweets, :timestamp => :tweets_timestamp do
134
+ TwitterApi.fetch_tweets_for @lady_gaga
135
+ end
136
+ ```
137
+
138
+ ```fetch_cache_of``` block uses same options as Rails.cache.fetch. You can easily add _expires_in_ option in it directly.
139
+
140
+ ```ruby
141
+ @house.fetch_cache :bricks, :expires_in => 1.minute do
142
+ ...
143
+ end
144
+ ```
145
+
146
+ Cache Machine stores timestamps for each of your model declared as resource in cache map.
147
+
148
+ ```ruby
149
+ House.timestamp
150
+ ```
151
+ Each time your houses collection is changed timestamp will change its value.
152
+ You can disable this callback in your cache map:
153
+
154
+ ```ruby
155
+ CacheMachine::Cache::Map.new.draw do
156
+ resource House, :timestamp => false
157
+ end
158
+ ```
159
+
160
+ ### Manual cache invalidation
161
+
162
+ ```ruby
163
+ # For classes.
164
+ House.reset_timestamp
165
+
166
+ # For collections.
167
+ @house.delete_cache :bricks
168
+
169
+ # For timestamps.
170
+ @house.reset_timestamp :bricks
171
+
172
+ # You can reset all associated caches using map.
173
+ @house.delete_all_caches
174
+ ```
175
+
176
+ ## Associations cache
177
+ You can fetch ids of an association from cache.
178
+
179
+ ```ruby
180
+ @house.association_ids(:bricks) # will return array of ids
181
+ ```
182
+ You can fetch associated objects from cache.
183
+
184
+ ```ruby
185
+ @house.associated_from_cache(:bricks) # will return scope of relation with condition to ids from cache map.
186
+ ```
187
+
188
+ ## ActionView helper
189
+ From examples above:
190
+
191
+ ```erb
192
+ <%= cache_for @lady_gaga, :upcoming_events, :timestamp => :each_hour do %>
193
+ <p>Don't hide yourself in regret
194
+ Just love yourself and you're set</p>
195
+ <% end %>
196
+ ```
197
+
198
+ ## Adapters
199
+ Cache Machine supports different types for storing cache:
200
+
201
+ - <b>cache map adapter</b> contains ids of relationships for each object from cache map
202
+ - <b>timestamps adapter</b> contains timestamps
203
+ - <b>content (storage) adapter</b> contains cached content itself (usually strings, html, etc)
204
+
205
+ You can setup custom adapters in your environment:
206
+
207
+ ```ruby
208
+ url = "redis://user:pass@host.com:9383/"
209
+ uri = URI.parse(url)
210
+ CacheMachine::Cache.timestamps_adapter = CacheMachine::Adapters::Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
211
+ CacheMachine::Cache.storage_adapter = CacheMachine::Adapters::Rails.new
212
+ CacheMachine::Cache.map_adapter = CacheMachine::Adapters::Rails.new
213
+ ```
214
+ Default adapter uses standard ```Rails.cache``` API.
215
+
216
+ Redis adapter is available in cache-machine-redis gem, please check out [here](http://github.com/zininserge/cache-machine-redis).
217
+
218
+ ## Rake tasks
219
+ Cache machine will produce SQL queries on each update in collection until all map of associations will stored in cache.
220
+ You can "prefill" cache map running:
221
+
222
+ ```rake cache_machine:fill_associations_map```
223
+
224
+ Rake accepts model names as params. Each of these models must be defined as resource in cache map:
225
+
226
+ ```ruby
227
+ CacheMachine::Cache::Map.new.draw do
228
+ resource House
229
+ resource Wall
230
+ end
231
+ ```
232
+
233
+ ```rake cache_machine:fill_associations_map[House, Wall]```
234
+
235
+ If all objects from database is too much for you, add additional scope to resource or collection:
236
+
237
+ ```ruby
238
+ CacheMachine::Cache::Map.new.draw do
239
+ resource House, :scope => :offices do
240
+ collection :bricks, :scope => :squared
241
+ end
242
+ resource Wall
243
+ end
244
+ ```
245
+
246
+ ## Contributing to cache-machine
247
+
248
+ 1. Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
249
+ 2. Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
250
+ 3. Fork the project
251
+ 4. Start a feature/bugfix branch
252
+ 5. Commit and push until you are happy with your contribution
253
+ 6. Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
254
+ 7. Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
255
+
256
+ ## Copyright
257
+
258
+ Copyright (c) 2011 PartyEarth LLC. See LICENSE.txt for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.10
1
+ 0.2.0
@@ -1,21 +1,15 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
1
  # -*- encoding: utf-8 -*-
5
2
 
6
3
  Gem::Specification.new do |s|
7
4
  s.name = %q{cache-machine}
8
- s.version = "0.1.10"
5
+ s.version = "0.2.0"
9
6
 
10
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
8
  s.authors = ["Sergei Zinin", "Kevin Goslar"]
12
- s.date = %q{2011-10-22}
9
+ s.date = %q{2012-03-05}
13
10
  s.description = %q{A Ruby on Rails framework to support cache management based on explicitely modeled caching dependencies.}
14
- s.email = %q{kgoslar@partyearth.com}
15
- s.extra_rdoc_files = [
16
- "LICENSE.txt",
17
- "README.rdoc"
18
- ]
11
+ s.email = %q{szinin@partyearth.com}
12
+ s.extra_rdoc_files = %w{LICENSE.txt README.md}
19
13
  s.files = `git ls-files`.split("\n")
20
14
  s.homepage = %q{http://github.com/partyearth/cache-machine}
21
15
  s.licenses = ["MIT"]
@@ -27,23 +21,14 @@ Gem::Specification.new do |s|
27
21
  s.specification_version = 3
28
22
 
29
23
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
30
- s.add_runtime_dependency(%q<rails>, [">= 0"])
31
- s.add_runtime_dependency(%q<activemodel>, [">= 0"])
32
- s.add_runtime_dependency(%q<activesupport>, [">= 0"])
24
+ s.add_runtime_dependency(%q<rails>, [">= 3"])
33
25
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
34
- s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
35
26
  else
36
- s.add_dependency(%q<rails>, [">= 0"])
37
- s.add_dependency(%q<activemodel>, [">= 0"])
38
- s.add_dependency(%q<activesupport>, [">= 0"])
27
+ s.add_dependency(%q<rails>, [">= 3"])
39
28
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
40
- s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
41
29
  end
42
30
  else
43
- s.add_dependency(%q<rails>, [">= 0"])
44
- s.add_dependency(%q<activemodel>, [">= 0"])
45
- s.add_dependency(%q<activesupport>, [">= 0"])
31
+ s.add_dependency(%q<rails>, [">= 3"])
46
32
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
47
- s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
48
33
  end
49
34
  end
@@ -0,0 +1 @@
1
+ .*db
@@ -1,10 +1,14 @@
1
1
  #--
2
2
  # Copyright (c) 2011 {PartyEarth LLC}[http://partyearth.com]
3
- # mailto:kgoslar@partyearth.com
3
+ # mailto:szinin@partyearth.com
4
4
  #++
5
+ require "cache_machine/cache/scope"
6
+ require "cache_machine/cache/timestamp_builder"
7
+ require "cache_machine/cache/associations"
8
+ require "cache_machine/cache/mapper"
5
9
  require "cache_machine/cache"
6
10
  require "cache_machine/helpers/cache_helper"
7
11
  require "cache_machine/logger"
8
12
  require "cache_machine/railtie"
9
13
 
10
- ActiveRecord::Base.send :include, CacheMachine::Cache
14
+ ActiveRecord::Base.send :include, CacheMachine::Cache
@@ -0,0 +1,127 @@
1
+ module CacheMachine
2
+ require "cache_machine/logger"
3
+
4
+ # API to your cache storage.
5
+ class Adapter
6
+
7
+ def initialize(*options)
8
+ raise 'not implemented yet'
9
+ end
10
+
11
+ # Appends an id to map of associations.
12
+ #
13
+ # @param target ActiveRecord object
14
+ # @param [ String, Symbol ] association Name of an association from target
15
+ # @param id Uniq identifier of any member from association of target
16
+ def append_id_to_map(target, association, id)
17
+ raise 'not implemented yet'
18
+ end
19
+
20
+ # Appends an id to map of associations in reverse-direction to used association.
21
+ #
22
+ # @param [ Class ] resource
23
+ # @param [ String, Symbol ] association Name of an association from target
24
+ # @param target
25
+ # @param id Uniq identifier of any member having class _resource_ of target association
26
+ def append_id_to_reverse_map(resource, association, target, id)
27
+ raise 'not implemented yet'
28
+ end
29
+
30
+ # Returns ids from cache.
31
+ #
32
+ # @param target
33
+ # @param [ String, Symbol ] association Collection name where we fetch ids.
34
+ #
35
+ # @return [ Array ]
36
+ def association_ids(target, association)
37
+ raise 'not implemented yet'
38
+ end
39
+
40
+ # Deletes key in cache.
41
+ #
42
+ # @param key
43
+ def delete(key)
44
+ raise 'not implemented yet'
45
+ end
46
+
47
+ # Deletes content from cache.
48
+ #
49
+ # @param key
50
+ def delete_content(key)
51
+ raise 'not implemented yet'
52
+ end
53
+
54
+ # Fetches cache.
55
+ #
56
+ # @param key
57
+ # @param [ Hash ] options
58
+ #
59
+ # @return [ Object ]
60
+ def fetch(key, options = {}, &block)
61
+ raise 'not implemented yet'
62
+ end
63
+
64
+ # Fetches timestamp from cache.
65
+ #
66
+ # @param [ String, Symbol ] name
67
+ # @param [ Hash ] options
68
+ #
69
+ # @return [ String ]
70
+ def fetch_timestamp(name, options = {}, &block)
71
+ raise 'not implemented yet'
72
+ end
73
+
74
+ # Returns content key used for fetch blocks.
75
+ #
76
+ # @param [ String, Symbol ] key
77
+ #
78
+ # @return String
79
+ def get_content_key(key)
80
+ "Content##{key}"
81
+ end
82
+
83
+ # Returns key used for associations map.
84
+ #
85
+ # @param target
86
+ # @param [ String, Symbol ] association
87
+ #
88
+ # @return String
89
+ def get_map_key(target, association)
90
+ "Map##{target.class.name}|#{target.send(target.class.primary_key)}|#{association}"
91
+ end
92
+
93
+ # Returns key to fetch timestamp from cache.
94
+ #
95
+ # @param [ String, Symbol ] name
96
+ #
97
+ # @return String
98
+ def get_timestamp_key(name)
99
+ "Timestamp##{name}"
100
+ end
101
+
102
+ # Returns key used for reversed associations map.
103
+ #
104
+ # @param [ Class ] resource
105
+ # @param [ String, Symbol ] association
106
+ # @param target
107
+ #
108
+ # @return String
109
+ def get_reverse_map_key(resource, association, target)
110
+ "ReverseMap##{resource}|#{target.send(target.class.primary_key)}|#{association}"
111
+ end
112
+
113
+ # Resets timestamp by name.
114
+ #
115
+ # @param [ String, Symbol ] name
116
+ def reset_timestamp(name)
117
+ raise 'not implemented yet'
118
+ end
119
+
120
+ # Writes timestamp in cache.
121
+ #
122
+ # @param [ String, Symbol ] name
123
+ def write_timestamp(name, &block)
124
+ raise 'not implemented yet'
125
+ end
126
+ end
127
+ end