ferblape-query_memcached 2.2.2
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/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +87 -0
- data/RUNNING_TESTS +18 -0
- data/Rakefile +37 -0
- data/TODO +6 -0
- data/init.rb +3 -0
- data/lib/extensions/lock.rb +31 -0
- data/lib/query_memcached.rb +191 -0
- data/test/query_memcached_test.rb +26 -0
- data/test/testing_app/README +1 -0
- data/test/testing_app/Rakefile +8 -0
- data/test/testing_app/app/controllers/application.rb +15 -0
- data/test/testing_app/app/helpers/application_helper.rb +3 -0
- data/test/testing_app/app/models/author.rb +133 -0
- data/test/testing_app/app/models/auto_id.rb +4 -0
- data/test/testing_app/app/models/binary.rb +2 -0
- data/test/testing_app/app/models/book.rb +4 -0
- data/test/testing_app/app/models/categorization.rb +5 -0
- data/test/testing_app/app/models/category.rb +29 -0
- data/test/testing_app/app/models/citation.rb +6 -0
- data/test/testing_app/app/models/club.rb +7 -0
- data/test/testing_app/app/models/column_name.rb +3 -0
- data/test/testing_app/app/models/comment.rb +25 -0
- data/test/testing_app/app/models/company.rb +123 -0
- data/test/testing_app/app/models/company_in_module.rb +61 -0
- data/test/testing_app/app/models/computer.rb +4 -0
- data/test/testing_app/app/models/contact.rb +16 -0
- data/test/testing_app/app/models/course.rb +3 -0
- data/test/testing_app/app/models/customer.rb +55 -0
- data/test/testing_app/app/models/default.rb +2 -0
- data/test/testing_app/app/models/developer.rb +76 -0
- data/test/testing_app/app/models/edge.rb +5 -0
- data/test/testing_app/app/models/entrant.rb +3 -0
- data/test/testing_app/app/models/guid.rb +2 -0
- data/test/testing_app/app/models/item.rb +7 -0
- data/test/testing_app/app/models/job.rb +5 -0
- data/test/testing_app/app/models/joke.rb +3 -0
- data/test/testing_app/app/models/keyboard.rb +3 -0
- data/test/testing_app/app/models/legacy_thing.rb +3 -0
- data/test/testing_app/app/models/matey.rb +4 -0
- data/test/testing_app/app/models/member.rb +9 -0
- data/test/testing_app/app/models/membership.rb +9 -0
- data/test/testing_app/app/models/minimalistic.rb +2 -0
- data/test/testing_app/app/models/mixed_case_monkey.rb +3 -0
- data/test/testing_app/app/models/movie.rb +5 -0
- data/test/testing_app/app/models/order.rb +4 -0
- data/test/testing_app/app/models/owner.rb +4 -0
- data/test/testing_app/app/models/parrot.rb +13 -0
- data/test/testing_app/app/models/person.rb +10 -0
- data/test/testing_app/app/models/pet.rb +4 -0
- data/test/testing_app/app/models/pirate.rb +9 -0
- data/test/testing_app/app/models/post.rb +80 -0
- data/test/testing_app/app/models/price_estimate.rb +3 -0
- data/test/testing_app/app/models/project.rb +29 -0
- data/test/testing_app/app/models/reader.rb +4 -0
- data/test/testing_app/app/models/reference.rb +4 -0
- data/test/testing_app/app/models/reply.rb +39 -0
- data/test/testing_app/app/models/ship.rb +3 -0
- data/test/testing_app/app/models/sponsor.rb +4 -0
- data/test/testing_app/app/models/subject.rb +4 -0
- data/test/testing_app/app/models/subscriber.rb +8 -0
- data/test/testing_app/app/models/subscription.rb +4 -0
- data/test/testing_app/app/models/tag.rb +7 -0
- data/test/testing_app/app/models/tagging.rb +10 -0
- data/test/testing_app/app/models/task.rb +2 -0
- data/test/testing_app/app/models/topic.rb +65 -0
- data/test/testing_app/app/models/treasure.rb +6 -0
- data/test/testing_app/app/models/vertex.rb +9 -0
- data/test/testing_app/app/models/warehouse_thing.rb +5 -0
- data/test/testing_app/config/boot.rb +109 -0
- data/test/testing_app/config/database.yml +11 -0
- data/test/testing_app/config/environment.rb +19 -0
- data/test/testing_app/config/environments/development.rb +0 -0
- data/test/testing_app/config/environments/production.rb +0 -0
- data/test/testing_app/config/environments/test.rb +0 -0
- data/test/testing_app/config/initializers/inflections.rb +10 -0
- data/test/testing_app/config/initializers/mime_types.rb +5 -0
- data/test/testing_app/config/initializers/new_rails_defaults.rb +15 -0
- data/test/testing_app/config/routes.rb +41 -0
- data/test/testing_app/db/schema.rb +443 -0
- data/test/testing_app/script/about +3 -0
- data/test/testing_app/script/console +3 -0
- data/test/testing_app/script/dbconsole +3 -0
- data/test/testing_app/script/destroy +3 -0
- data/test/testing_app/script/generate +3 -0
- data/test/testing_app/script/performance/benchmarker +3 -0
- data/test/testing_app/script/performance/profiler +3 -0
- data/test/testing_app/script/performance/request +3 -0
- data/test/testing_app/script/plugin +3 -0
- data/test/testing_app/script/process/inspector +3 -0
- data/test/testing_app/script/process/reaper +3 -0
- data/test/testing_app/script/process/spawner +3 -0
- data/test/testing_app/script/runner +3 -0
- data/test/testing_app/script/server +3 -0
- data/test/testing_app/test/fixtures/accounts.yml +28 -0
- data/test/testing_app/test/fixtures/all/developers.yml +0 -0
- data/test/testing_app/test/fixtures/all/people.csv +0 -0
- data/test/testing_app/test/fixtures/all/tasks.yml +0 -0
- data/test/testing_app/test/fixtures/author_addresses.yml +5 -0
- data/test/testing_app/test/fixtures/author_favorites.yml +4 -0
- data/test/testing_app/test/fixtures/authors.yml +9 -0
- data/test/testing_app/test/fixtures/binaries.yml +132 -0
- data/test/testing_app/test/fixtures/books.yml +7 -0
- data/test/testing_app/test/fixtures/categories.yml +14 -0
- data/test/testing_app/test/fixtures/categories/special_categories.yml +9 -0
- data/test/testing_app/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/testing_app/test/fixtures/categories_posts.yml +23 -0
- data/test/testing_app/test/fixtures/categorizations.yml +17 -0
- data/test/testing_app/test/fixtures/clubs.yml +6 -0
- data/test/testing_app/test/fixtures/comments.yml +59 -0
- data/test/testing_app/test/fixtures/companies.yml +55 -0
- data/test/testing_app/test/fixtures/computers.yml +4 -0
- data/test/testing_app/test/fixtures/courses.yml +7 -0
- data/test/testing_app/test/fixtures/customers.yml +17 -0
- data/test/testing_app/test/fixtures/developers.yml +21 -0
- data/test/testing_app/test/fixtures/developers_projects.yml +17 -0
- data/test/testing_app/test/fixtures/edges.yml +6 -0
- data/test/testing_app/test/fixtures/entrants.yml +14 -0
- data/test/testing_app/test/fixtures/fk_test_has_fk.yml +3 -0
- data/test/testing_app/test/fixtures/fk_test_has_pk.yml +2 -0
- data/test/testing_app/test/fixtures/funny_jokes.yml +10 -0
- data/test/testing_app/test/fixtures/items.yml +4 -0
- data/test/testing_app/test/fixtures/jobs.yml +7 -0
- data/test/testing_app/test/fixtures/legacy_things.yml +3 -0
- data/test/testing_app/test/fixtures/mateys.yml +4 -0
- data/test/testing_app/test/fixtures/members.yml +4 -0
- data/test/testing_app/test/fixtures/memberships.yml +20 -0
- data/test/testing_app/test/fixtures/minimalistics.yml +2 -0
- data/test/testing_app/test/fixtures/mixed_case_monkeys.yml +6 -0
- data/test/testing_app/test/fixtures/mixins.yml +29 -0
- data/test/testing_app/test/fixtures/movies.yml +7 -0
- data/test/testing_app/test/fixtures/naked/csv/accounts.csv +1 -0
- data/test/testing_app/test/fixtures/naked/yml/accounts.yml +1 -0
- data/test/testing_app/test/fixtures/naked/yml/companies.yml +1 -0
- data/test/testing_app/test/fixtures/naked/yml/courses.yml +1 -0
- data/test/testing_app/test/fixtures/owners.yml +7 -0
- data/test/testing_app/test/fixtures/parrots.yml +27 -0
- data/test/testing_app/test/fixtures/parrots_pirates.yml +7 -0
- data/test/testing_app/test/fixtures/people.yml +6 -0
- data/test/testing_app/test/fixtures/pets.yml +14 -0
- data/test/testing_app/test/fixtures/pirates.yml +9 -0
- data/test/testing_app/test/fixtures/posts.yml +49 -0
- data/test/testing_app/test/fixtures/price_estimates.yml +7 -0
- data/test/testing_app/test/fixtures/projects.yml +7 -0
- data/test/testing_app/test/fixtures/readers.yml +9 -0
- data/test/testing_app/test/fixtures/references.yml +17 -0
- data/test/testing_app/test/fixtures/reserved_words/distinct.yml +5 -0
- data/test/testing_app/test/fixtures/reserved_words/distincts_selects.yml +11 -0
- data/test/testing_app/test/fixtures/reserved_words/group.yml +14 -0
- data/test/testing_app/test/fixtures/reserved_words/select.yml +8 -0
- data/test/testing_app/test/fixtures/reserved_words/values.yml +7 -0
- data/test/testing_app/test/fixtures/ships.yml +5 -0
- data/test/testing_app/test/fixtures/sponsors.yml +9 -0
- data/test/testing_app/test/fixtures/subscribers.yml +7 -0
- data/test/testing_app/test/fixtures/subscriptions.yml +12 -0
- data/test/testing_app/test/fixtures/taggings.yml +28 -0
- data/test/testing_app/test/fixtures/tags.yml +7 -0
- data/test/testing_app/test/fixtures/tasks.yml +7 -0
- data/test/testing_app/test/fixtures/topics.yml +42 -0
- data/test/testing_app/test/fixtures/treasures.yml +10 -0
- data/test/testing_app/test/fixtures/vertices.yml +4 -0
- data/test/testing_app/test/fixtures/warehouse-things.yml +3 -0
- data/test/testing_app/test/test_helper.rb +131 -0
- data/test/testing_app/test/unit/query_cache_test.rb +180 -0
- metadata +289 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 [Fernando Blat]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# QueryMemcached
|
2
|
+
|
3
|
+
<http://github.com/ferblape/query_memcached>
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
In the root directory of your project:
|
8
|
+
|
9
|
+
script/plugin install git://github.com/ferblape/query_memcached.git
|
10
|
+
|
11
|
+
If you want to install it like a git module you can follow this post, adapting the URLs to this plugin:
|
12
|
+
|
13
|
+
<http://woss.name/2008/04/09/using-git-submodules-to-track-vendorrails/>
|
14
|
+
|
15
|
+
## Requirements
|
16
|
+
|
17
|
+
- 1.9 >= Ruby >= 1.8.4
|
18
|
+
|
19
|
+
- 2.2.2 >= Rails >= 2.1
|
20
|
+
|
21
|
+
- memcache-client gem (maybe it will work also in memcached gem by fauna, but I haven't tested it yet)
|
22
|
+
|
23
|
+
- MySQL or PostgreSQL
|
24
|
+
|
25
|
+
## Description
|
26
|
+
|
27
|
+
This plugin tries to replace ActiveRecord query_cache, adding a Memcache layer for persistence of the query's cache between requests.
|
28
|
+
|
29
|
+
It is, instead of saving all the SQL query's results in memory each request you save them into Memcached, so you have them all the time.
|
30
|
+
|
31
|
+
For expiring this cache, each Memcached key contains a sum of all the version numbers of the tables involved in the query. If one of that tables is modified, then the version number for that table is increased.
|
32
|
+
|
33
|
+
For example, the query below involves the table _items_ and the table _places_:
|
34
|
+
|
35
|
+
SELECT * from items INNER JOIN places ON places.item_id = item.id WHERE (item.created_at >= '2008-02-02 00:00:00')
|
36
|
+
|
37
|
+
So, the version for the cache of that query will be the sum of the cache version of the table _items_ and the cache version of the table _places_.
|
38
|
+
|
39
|
+
The idea behind this is way of create and expire cache was inspired by the post [The Secret to Memcached](http://blog.leetsoft.com/2007/5/22/the-secret-to-memcached) by Tobias Lütke.
|
40
|
+
|
41
|
+
For this plugin to work it's supposed that your ActionController cache store configured is `mem_cache_store`.
|
42
|
+
|
43
|
+
I changed the behavior in one major way. I made the memcaching of the QueryCache optional.
|
44
|
+
|
45
|
+
You need to add `enable_memache_querycache` to your AcitveRecord model, like so:
|
46
|
+
|
47
|
+
class User < ActiveRecord::Base
|
48
|
+
enable_memcache_querycache :expires_in => 10.minutes
|
49
|
+
end
|
50
|
+
|
51
|
+
Additionally you can indicate in what time should expire the cache. 90 minutes is the default value.
|
52
|
+
|
53
|
+
The reason for this (drastic) change is two fold:
|
54
|
+
|
55
|
+
- For starters, there are many tables where trying to cache the contents but expiring all caching on any insert/update/delete/drop/alter on the table causes unnecessary overhead. A sessions table is a perfect example. I also have a metrics table and a few other tables where the contents are changed _often_. By not enabling memcache on tables that I know will constantly be changing I can save quite a number of needless memcache calls (not caching the session queries saves two reads and two writes per request).
|
56
|
+
|
57
|
+
- The other reason is I don't quite trust the implications of having a persisted query cache. I want to carefully roll it out, starting with just the few models that rarely change, and go from there. I'm not worried about users seeing information they shouldn't be, as the key is the query; it is more about making sure things expire correctly. I didn't want to push such a large caching change into my app without a careful (and long) rollout.
|
58
|
+
|
59
|
+
## Known issues
|
60
|
+
|
61
|
+
- You can get race conditions in the version keys of tables stored in Memcached
|
62
|
+
|
63
|
+
## Running plugin tests
|
64
|
+
|
65
|
+
The tests of this plugin are not so standard so I decided to wrote a very explicative instructions in a file apart named RUNNING_TESTS
|
66
|
+
|
67
|
+
## TODO
|
68
|
+
|
69
|
+
There is a list of pending features, bugs, and so on in a file named TODO.
|
70
|
+
|
71
|
+
Any comments and suggestions are welcome.
|
72
|
+
|
73
|
+
## Another comments
|
74
|
+
|
75
|
+
It's so easy to adapt the plugin for Rails version 2.0.2 if you change the cache variable for another instantiated with the memcache-client.
|
76
|
+
|
77
|
+
Also, it is possible to run in Rails >= 1.2.4 if you change the plugin [query\_cache](http://agilewebdevelopment.com/plugins/query_cache) with the changes of query_memcached.
|
78
|
+
|
79
|
+
## Special Thanks
|
80
|
+
|
81
|
+
- Raul Murciano <http://raul.murciano.net/> for helping to adapt the plugin to Rails 2.1
|
82
|
+
|
83
|
+
- methodmissing <http://blog.methodmissing.com/> for some fresh ideas, lock library and correct some mistakes
|
84
|
+
|
85
|
+
- skippy <http://github.com/skippy> for a lot of work deleting and cleaning methods, and also doing this plugin optional to each model
|
86
|
+
|
87
|
+
Copyright (c) 2008 [Fernando Blat](http://www.inwebwetrust.net), released under the MIT license
|
data/RUNNING_TESTS
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= Running tests
|
2
|
+
|
3
|
+
For testing this plugin I decided to adapt a the query cache tests' from ActiveRecord.
|
4
|
+
|
5
|
+
First of all you have to know that there is a Rails 2.1 application in `test/testing_app`.
|
6
|
+
|
7
|
+
So you need a database as indicated in `config/database.yml`:
|
8
|
+
|
9
|
+
user: root
|
10
|
+
adapter: mysql
|
11
|
+
database: 'activerecord_unittest'
|
12
|
+
encoding: utf8
|
13
|
+
|
14
|
+
Then, load the schema from `db/schema.rb`, and launch a memcache server in standar port 11211.
|
15
|
+
|
16
|
+
Now, you can go to the plugin root directory and perform the test task:
|
17
|
+
|
18
|
+
rake test
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the query_memcached plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the query_memcached plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'QueryMemcached'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
require 'jeweler'
|
26
|
+
Jeweler::Tasks.new do |gem|
|
27
|
+
gem.name = "query_memcached"
|
28
|
+
gem.summary = "A replacement for ActiveRecord query_cache that a adds a Memcache layer for persistence of the query's cache"
|
29
|
+
gem.email = ["ferblape@gmail.com"]
|
30
|
+
gem.homepage = "http://github.com/ferblape/query_memcached"
|
31
|
+
gem.authors = ["Fernando Blat"]
|
32
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
33
|
+
end
|
34
|
+
|
35
|
+
rescue LoadError
|
36
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
37
|
+
end
|
data/TODO
ADDED
data/init.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Experimental LOCK support ported form http://github.com/fauna/interlock/tree/master/lib/interlock/lock.rb
|
2
|
+
module ActiveSupport
|
3
|
+
module Cache
|
4
|
+
class MemCacheStore < Store
|
5
|
+
|
6
|
+
def lock(key, lock_expiry = 30, retries = 5)
|
7
|
+
retries.times do |count|
|
8
|
+
|
9
|
+
begin
|
10
|
+
response = @data.add("lock:#{key}", "Locked by #{Process.pid}", lock_expiry)
|
11
|
+
response ||= Response::STORED
|
12
|
+
rescue Object => e
|
13
|
+
end
|
14
|
+
|
15
|
+
if response == Response::STORED
|
16
|
+
begin
|
17
|
+
value = yield( @data.get(key) )
|
18
|
+
@data.set(key, value)
|
19
|
+
return value
|
20
|
+
ensure
|
21
|
+
@data.delete("lock:#{key}")
|
22
|
+
end
|
23
|
+
else
|
24
|
+
sleep((2**count) / 2.0)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
# table_names is a special attribute that contains a regular expression with all the tables of the application
|
8
|
+
# Its main purpose is to detect all the tables that a query affects.
|
9
|
+
# It is build in a special way:
|
10
|
+
# - first we get all the tables
|
11
|
+
# - then we sort them from major to minor lenght, in order to detect tables which name is a composition of two
|
12
|
+
# names, i.e, posts, comments and comments_posts. It is for make easier the regular expression
|
13
|
+
# - and finally, the regular expression is built
|
14
|
+
cattr_accessor :table_names, :enableMemcacheQueryForModels
|
15
|
+
|
16
|
+
self.table_names = /#{connection.tables.sort_by { |c| c.length }.join('|')}/i
|
17
|
+
self.enableMemcacheQueryForModels ||= {}
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
# put this class method at the top of your AR model to enable memcache for the queryCache,
|
22
|
+
# otherwise it will use standard query cache
|
23
|
+
def enable_memcache_querycache(options = {})
|
24
|
+
if ActionController::Base.perform_caching && defined?(::Rails.cache) && ::Rails.cache.is_a?(ActiveSupport::Cache::MemCacheStore)
|
25
|
+
options[:expires_in] ||= 90.minutes
|
26
|
+
self.enableMemcacheQueryForModels[ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s] = options
|
27
|
+
else
|
28
|
+
warning = "[Query memcached WARNING] Disabled for #{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self)} -- Memcache for QueryCache is not enabled for this model because caching is not turned on, Rails.cache is not defined, or cache engine is not mem_cache_store"
|
29
|
+
ActiveRecord::Base.logger.error warning
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def connection_with_memcache_query_cache
|
34
|
+
conn = connection_without_memcache_query_cache
|
35
|
+
conn.memcache_query_cache_options = self.enableMemcacheQueryForModels[self.to_s]
|
36
|
+
conn
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method_chain :connection, :memcache_query_cache
|
40
|
+
|
41
|
+
def cache_version_key(table_name = nil)
|
42
|
+
"#{global_cache_version_key}/#{table_name || self.table_name}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def global_cache_version_key; 'version' end
|
46
|
+
|
47
|
+
# Increment the class version key number
|
48
|
+
def increase_version!(table_name = nil)
|
49
|
+
key = cache_version_key(table_name)
|
50
|
+
if r = ::Rails.cache.read(key)
|
51
|
+
::Rails.cache.write(key, r.to_i + 1)
|
52
|
+
else
|
53
|
+
::Rails.cache.write(key, 1)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Given a sql query this method extract all the table names of the database affected by the query
|
58
|
+
# thanks to the regular expression we have generated on the load of the plugin
|
59
|
+
def extract_table_names(sql)
|
60
|
+
sql.gsub(/`/,'').scan(self.table_names).map {|t| t.strip}.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
module ConnectionAdapters # :nodoc:
|
68
|
+
|
69
|
+
class AbstractAdapter
|
70
|
+
attr_accessor :memcache_query_cache_options
|
71
|
+
end
|
72
|
+
|
73
|
+
class MysqlAdapter < AbstractAdapter
|
74
|
+
|
75
|
+
# alias_method_chain for expiring cache if necessary
|
76
|
+
def execute_with_clean_query_cache(*args)
|
77
|
+
return execute_without_clean_query_cache(*args) unless self.memcache_query_cache_options && query_cache_enabled
|
78
|
+
sql = args[0].strip
|
79
|
+
if sql =~ /^(INSERT|UPDATE|ALTER|DROP|DELETE)/i
|
80
|
+
# can only modify one table at a time...so stop after matching the first table name
|
81
|
+
table_name = ActiveRecord::Base.extract_table_names(sql).first
|
82
|
+
ActiveRecord::Base.increase_version!(table_name)
|
83
|
+
end
|
84
|
+
execute_without_clean_query_cache(*args)
|
85
|
+
end
|
86
|
+
|
87
|
+
alias_method_chain :execute, :clean_query_cache
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class PostgreSQLAdapter < AbstractAdapter
|
92
|
+
|
93
|
+
def execute_with_clean_query_cache(*args)
|
94
|
+
return execute_without_clean_query_cache(*args) unless self.memcache_query_cache_options && query_cache_enabled
|
95
|
+
sql = args[0].strip
|
96
|
+
if sql =~ /^(INSERT|UPDATE|ALTER|DROP|DELETE)/i
|
97
|
+
table_name = ActiveRecord::Base.extract_table_names(sql).first
|
98
|
+
ActiveRecord::Base.increase_version!(table_name)
|
99
|
+
end
|
100
|
+
execute_without_clean_query_cache(*args)
|
101
|
+
end
|
102
|
+
|
103
|
+
alias_method_chain :execute, :clean_query_cache
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
module QueryCache
|
108
|
+
|
109
|
+
# Enable the query cache within the block
|
110
|
+
def cache
|
111
|
+
old, @query_cache_enabled = @query_cache_enabled, true
|
112
|
+
@query_cache ||= {}
|
113
|
+
@cache_version ||= {}
|
114
|
+
yield
|
115
|
+
ensure
|
116
|
+
@query_cache_enabled = old
|
117
|
+
clear_query_cache
|
118
|
+
end
|
119
|
+
|
120
|
+
# Clears the query cache.
|
121
|
+
#
|
122
|
+
# One reason you may wish to call this method explicitly is between queries
|
123
|
+
# that ask the database to randomize results. Otherwise the cache would see
|
124
|
+
# the same SQL query and repeatedly return the same result each time, silently
|
125
|
+
# undermining the randomness you were expecting.
|
126
|
+
def clear_query_cache
|
127
|
+
@query_cache.clear if @query_cache
|
128
|
+
@cache_version.clear if @cache_version
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def cache_sql(sql)
|
134
|
+
# priority order:
|
135
|
+
# - if in @query_cache (memory of local app server) we prefer this
|
136
|
+
# - else if exists in Memcached we prefer that
|
137
|
+
# - else perform query in database and save memory caches
|
138
|
+
result =
|
139
|
+
if (query_cache_enabled || self.memcache_query_cache_options) && @query_cache.has_key?(sql)
|
140
|
+
log_info(sql, "CACHE", 0.0)
|
141
|
+
@query_cache[sql]
|
142
|
+
elsif self.memcache_query_cache_options && cached_result = ::Rails.cache.read(query_key(sql), self.memcache_query_cache_options)
|
143
|
+
log_info(sql, "MEMCACHE", 0.0)
|
144
|
+
@query_cache[sql] = cached_result
|
145
|
+
else
|
146
|
+
query_result = yield
|
147
|
+
@query_cache[sql] = query_result if query_cache_enabled || self.memcache_query_cache_options
|
148
|
+
::Rails.cache.write(query_key(sql), query_result, self.memcache_query_cache_options) if self.memcache_query_cache_options
|
149
|
+
query_result
|
150
|
+
end
|
151
|
+
|
152
|
+
if Array === result
|
153
|
+
result.collect { |row| row.dup }
|
154
|
+
else
|
155
|
+
result.duplicable? ? result.dup : result
|
156
|
+
end
|
157
|
+
rescue TypeError
|
158
|
+
result
|
159
|
+
end
|
160
|
+
|
161
|
+
# Transforms a sql query into a valid key for Memcache
|
162
|
+
def query_key(sql)
|
163
|
+
table_names = ActiveRecord::Base.extract_table_names(sql)
|
164
|
+
# version_number is the sum of the global version number and all
|
165
|
+
# the version numbers of each table
|
166
|
+
version_number = get_cache_version # global version
|
167
|
+
table_names.each { |table_name| version_number += get_cache_version(table_name) }
|
168
|
+
"#{version_number}_#{Digest::MD5.hexdigest(sql)}"
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns the cache version of a table_name. If table_name is empty its the global version
|
172
|
+
#
|
173
|
+
# We prefer to search for this key first in memory and then in Memcache
|
174
|
+
def get_cache_version(table_name = nil)
|
175
|
+
key_class_version = table_name ? ActiveRecord::Base.cache_version_key(table_name) : ActiveRecord::Base.global_cache_version_key
|
176
|
+
if @cache_version && @cache_version[key_class_version]
|
177
|
+
@cache_version[key_class_version]
|
178
|
+
elsif version = ::Rails.cache.read(key_class_version)
|
179
|
+
@cache_version[key_class_version] = version if @cache_version
|
180
|
+
version
|
181
|
+
else
|
182
|
+
@cache_version[key_class_version] = 0 if @cache_version
|
183
|
+
::Rails.cache.write(key_class_version, 0)
|
184
|
+
0
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '/testing_app/config/environment.rb'))
|
3
|
+
|
4
|
+
class QueryMemcachedTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_extract_table_names
|
7
|
+
p = "select * from pets"
|
8
|
+
q = "select * from author_favorites where (author_favorites.author_id = 36) AND (author_favorites.user_id = 11) LIMIT 1"
|
9
|
+
r = "SELECT * from binary_fields where (binary_fields.place_id = 3)"
|
10
|
+
s = "select distinct(p.id) from pets p, binary_fields u, binary_fields pu, where pu.user_id = u.id and pu.place_id = p.id and p.id != 3"
|
11
|
+
t = "select count(*) as count_all from pets inner join binary_fields on pets_id = binary_fields.place_id where (place_id = 3) AND (binary_fields.user_id = 11)"
|
12
|
+
u = "select * from categories order by created_at DESC limit 10"
|
13
|
+
v = "SELECT * from pets where id IN (SELECT * from pets where place_id = pets.id)"
|
14
|
+
w = "SELECT categories.* FROM categories INNER JOIN binary_fields ON binary_fields.id = categories.user_id WHERE ((binary_fields.contact_id = 1))"
|
15
|
+
|
16
|
+
assert_equal ['pets'], ActiveRecord::Base.extract_table_names(p)
|
17
|
+
assert_equal ['author_favorites'], ActiveRecord::Base.extract_table_names(q)
|
18
|
+
assert_equal ['binary_fields'], ActiveRecord::Base.extract_table_names(r)
|
19
|
+
assert_equal ['pets', 'binary_fields'], ActiveRecord::Base.extract_table_names(s)
|
20
|
+
assert_equal ['pets', 'binary_fields'], ActiveRecord::Base.extract_table_names(t)
|
21
|
+
assert_equal ['categories'], ActiveRecord::Base.extract_table_names(u)
|
22
|
+
assert_equal ['pets'], ActiveRecord::Base.extract_table_names(v)
|
23
|
+
assert_equal ['categories', 'binary_fields'], ActiveRecord::Base.extract_table_names(w)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# Testing application from plugin Query Memcached
|