artemk-cache-money 0.2.13.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/LICENSE +201 -0
- data/README +210 -0
- data/README.markdown +210 -0
- data/TODO +17 -0
- data/UNSUPPORTED_FEATURES +12 -0
- data/config/environment.rb +16 -0
- data/config/memcached.yml +6 -0
- data/db/schema.rb +18 -0
- data/init.rb +1 -0
- data/lib/cache_money.rb +86 -0
- data/lib/cash/accessor.rb +83 -0
- data/lib/cash/buffered.rb +129 -0
- data/lib/cash/config.rb +82 -0
- data/lib/cash/fake.rb +83 -0
- data/lib/cash/finders.rb +38 -0
- data/lib/cash/index.rb +214 -0
- data/lib/cash/local.rb +76 -0
- data/lib/cash/lock.rb +63 -0
- data/lib/cash/mock.rb +154 -0
- data/lib/cash/query/abstract.rb +197 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +50 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/request.rb +3 -0
- data/lib/cash/transactional.rb +43 -0
- data/lib/cash/util/active_record.rb +5 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/util/marshal.rb +19 -0
- data/lib/cash/write_through.rb +69 -0
- data/lib/mem_cached_session_store.rb +50 -0
- data/lib/mem_cached_support_store.rb +141 -0
- data/lib/memcached_wrapper.rb +261 -0
- data/spec/cash/accessor_spec.rb +186 -0
- data/spec/cash/active_record_spec.rb +224 -0
- data/spec/cash/buffered_spec.rb +9 -0
- data/spec/cash/calculations_spec.rb +67 -0
- data/spec/cash/finders_spec.rb +408 -0
- data/spec/cash/local_buffer_spec.rb +9 -0
- data/spec/cash/local_spec.rb +9 -0
- data/spec/cash/lock_spec.rb +108 -0
- data/spec/cash/marshal_spec.rb +60 -0
- data/spec/cash/order_spec.rb +172 -0
- data/spec/cash/transactional_spec.rb +578 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +245 -0
- data/spec/memcached_wrapper_test.rb +209 -0
- data/spec/spec_helper.rb +68 -0
- metadata +168 -0
data/TODO
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
TOP PRIORITY
|
2
|
+
|
3
|
+
REFACTOR
|
4
|
+
* Clarify terminology around cache/key/index, etc.
|
5
|
+
|
6
|
+
INFRASTRUCTURE
|
7
|
+
|
8
|
+
NEW FEATURES
|
9
|
+
* transactional get multi isn't really multi
|
10
|
+
|
11
|
+
BUGS
|
12
|
+
* Handle append strategy (using add rather than set?) to avoid race condition
|
13
|
+
|
14
|
+
MISSING TESTS:
|
15
|
+
* missing tests for Klass.transaction do ... end
|
16
|
+
* non "id" pks work but lack test coverage
|
17
|
+
* expire_cache
|
@@ -0,0 +1,12 @@
|
|
1
|
+
* does not work with :dependent => nullify because
|
2
|
+
def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
|
3
|
+
association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
|
4
|
+
end
|
5
|
+
This does not trigger callbacks
|
6
|
+
* update_all, delete, update_counter, increment_counter, decrement_counter, counter_caches in general - counter caches are replaced by this gem, bear that in mind.
|
7
|
+
* attr_readonly - no technical obstacle, just not yet supported
|
8
|
+
* attributes before typecast behave unpredictably - hard to support
|
9
|
+
* printf style binds: :conditions => ["name = '%s'", "37signals!"] - not too hard to support
|
10
|
+
* objects as attributes that are serialized. story.title = {:foo => :bar}; customer.balance = Money.new(...) - these could be coerced using Column#type_cast?
|
11
|
+
|
12
|
+
With a lot of these features the issue is not technical but performance. Every special case costs some overhead.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'activesupport', '~> 2.3.0'
|
3
|
+
gem 'activerecord', '~> 2.3.0'
|
4
|
+
gem 'actionpack', '~> 2.3.0'
|
5
|
+
gem 'rspec', '>= 1.3.0'
|
6
|
+
gem 'jeweler', '~> 1.4.0'
|
7
|
+
|
8
|
+
require 'action_controller'
|
9
|
+
require 'active_record'
|
10
|
+
require 'active_record/session_store'
|
11
|
+
require 'jeweler'
|
12
|
+
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
:adapter => 'sqlite3',
|
15
|
+
:database => ':memory:'
|
16
|
+
)
|
data/db/schema.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 2) do
|
2
|
+
create_table "stories", :force => true do |t|
|
3
|
+
t.string "title", "subtitle"
|
4
|
+
t.string "type"
|
5
|
+
t.boolean "published"
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table "characters", :force => true do |t|
|
9
|
+
t.integer "story_id"
|
10
|
+
t.string "name"
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :sessions, :force => true do |t|
|
14
|
+
t.string :session_id
|
15
|
+
t.text :data
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'cache_money'
|
data/lib/cache_money.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
require 'cash/lock'
|
5
|
+
require 'cash/transactional'
|
6
|
+
require 'cash/write_through'
|
7
|
+
require 'cash/finders'
|
8
|
+
require 'cash/buffered'
|
9
|
+
require 'cash/index'
|
10
|
+
require 'cash/config'
|
11
|
+
require 'cash/accessor'
|
12
|
+
|
13
|
+
require 'cash/request'
|
14
|
+
require 'cash/fake'
|
15
|
+
require 'cash/local'
|
16
|
+
|
17
|
+
require 'cash/query/abstract'
|
18
|
+
require 'cash/query/select'
|
19
|
+
require 'cash/query/primary_key'
|
20
|
+
require 'cash/query/calculation'
|
21
|
+
|
22
|
+
require 'cash/util/array'
|
23
|
+
require 'cash/util/marshal'
|
24
|
+
|
25
|
+
class ActiveRecord::Base
|
26
|
+
def self.is_cached(options = {})
|
27
|
+
if options == false
|
28
|
+
include NoCash
|
29
|
+
else
|
30
|
+
options.assert_valid_keys(:ttl, :repository, :version)
|
31
|
+
include Cash unless ancestors.include?(Cash)
|
32
|
+
Cash::Config.create(self, options)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def <=>(other)
|
37
|
+
if self.id == other.id then
|
38
|
+
0
|
39
|
+
else
|
40
|
+
self.id < other.id ? -1 : 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Cash
|
46
|
+
def self.included(active_record_class)
|
47
|
+
active_record_class.class_eval do
|
48
|
+
include Config, Accessor, WriteThrough, Finders
|
49
|
+
extend ClassMethods
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
def self.extended(active_record_class)
|
55
|
+
class << active_record_class
|
56
|
+
alias_method_chain :transaction, :cache_transaction
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def transaction_with_cache_transaction(&block)
|
61
|
+
if cache_config
|
62
|
+
transaction_without_cache_transaction do
|
63
|
+
repository.transaction(&block)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
transaction_without_cache_transaction(&block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def cacheable?(*args)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
module NoCash
|
76
|
+
def self.included(active_record_class)
|
77
|
+
active_record_class.class_eval do
|
78
|
+
extend ClassMethods
|
79
|
+
end
|
80
|
+
end
|
81
|
+
module ClassMethods
|
82
|
+
def cachable?(*args)
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Cash
|
2
|
+
module Accessor
|
3
|
+
def self.included(a_module)
|
4
|
+
a_module.module_eval do
|
5
|
+
extend ClassMethods
|
6
|
+
include InstanceMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def fetch(keys, options = {}, &block)
|
12
|
+
case keys
|
13
|
+
when Array
|
14
|
+
return {} if keys.empty?
|
15
|
+
|
16
|
+
keys = keys.collect { |key| cache_key(key) }
|
17
|
+
hits = repository.get_multi(*keys)
|
18
|
+
if (missed_keys = keys - hits.keys).any?
|
19
|
+
missed_values = block.call(missed_keys)
|
20
|
+
hits.merge!(missed_keys.zip(Array(missed_values)).to_hash_without_nils)
|
21
|
+
end
|
22
|
+
hits
|
23
|
+
else
|
24
|
+
repository.get(cache_key(keys), options[:raw]) || (block ? block.call : nil)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def get(keys, options = {}, &block)
|
29
|
+
case keys
|
30
|
+
when Array
|
31
|
+
fetch(keys, options, &block)
|
32
|
+
else
|
33
|
+
fetch(keys, options) do
|
34
|
+
if block_given?
|
35
|
+
add(keys, result = yield(keys), options)
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(key, value, options = {})
|
43
|
+
if repository.add(cache_key(key), value, options[:ttl] || cache_config.ttl, options[:raw]) == "NOT_STORED\r\n"
|
44
|
+
yield if block_given?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def set(key, value, options = {})
|
49
|
+
repository.set(cache_key(key), value, options[:ttl] || cache_config.ttl, options[:raw])
|
50
|
+
end
|
51
|
+
|
52
|
+
def incr(key, delta = 1, ttl = nil)
|
53
|
+
ttl ||= cache_config.ttl
|
54
|
+
repository.incr(cache_key = cache_key(key), delta) || begin
|
55
|
+
repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.incr(cache_key) }
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def decr(key, delta = 1, ttl = nil)
|
61
|
+
ttl ||= cache_config.ttl
|
62
|
+
repository.decr(cache_key = cache_key(key), delta) || begin
|
63
|
+
repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.decr(cache_key) }
|
64
|
+
result
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def expire(key)
|
69
|
+
repository.delete(cache_key(key))
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache_key(key)
|
73
|
+
"#{name}:#{cache_config.version}/#{key.to_s.gsub(' ', '+')}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module InstanceMethods
|
78
|
+
def expire
|
79
|
+
self.class.expire(id)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Cash
|
2
|
+
class Buffered
|
3
|
+
def self.push(cache, lock)
|
4
|
+
if cache.is_a?(Buffered)
|
5
|
+
cache.push
|
6
|
+
else
|
7
|
+
Buffered.new(cache, lock)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(memcache, lock)
|
12
|
+
@buffer = {}
|
13
|
+
@commands = []
|
14
|
+
@cache = memcache
|
15
|
+
@lock = lock
|
16
|
+
end
|
17
|
+
|
18
|
+
def pop
|
19
|
+
@cache
|
20
|
+
end
|
21
|
+
|
22
|
+
def push
|
23
|
+
NestedBuffered.new(self, @lock)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(key, *options)
|
27
|
+
if @buffer.has_key?(key)
|
28
|
+
@buffer[key]
|
29
|
+
else
|
30
|
+
@buffer[key] = @cache.get(key, *options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def set(key, value, *options)
|
35
|
+
@buffer[key] = value
|
36
|
+
buffer_command Command.new(:set, key, value, *options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def incr(key, amount = 1)
|
40
|
+
return unless value = get(key, true)
|
41
|
+
|
42
|
+
@buffer[key] = value.to_i + amount
|
43
|
+
buffer_command Command.new(:incr, key, amount)
|
44
|
+
@buffer[key]
|
45
|
+
end
|
46
|
+
|
47
|
+
def decr(key, amount = 1)
|
48
|
+
return unless value = get(key, true)
|
49
|
+
|
50
|
+
@buffer[key] = [value.to_i - amount, 0].max
|
51
|
+
buffer_command Command.new(:decr, key, amount)
|
52
|
+
@buffer[key]
|
53
|
+
end
|
54
|
+
|
55
|
+
def add(key, value, *options)
|
56
|
+
@buffer[key] = value
|
57
|
+
buffer_command Command.new(:add, key, value, *options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(key, *options)
|
61
|
+
@buffer[key] = nil
|
62
|
+
buffer_command Command.new(:delete, key, *options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_multi(*keys)
|
66
|
+
values = keys.collect { |key| get(key) }
|
67
|
+
keys.zip(values).to_hash_without_nils
|
68
|
+
end
|
69
|
+
|
70
|
+
def flush
|
71
|
+
sorted_keys = @commands.select(&:requires_lock?).collect(&:key).uniq.sort
|
72
|
+
sorted_keys.each do |key|
|
73
|
+
@lock.acquire_lock(key)
|
74
|
+
end
|
75
|
+
perform_commands
|
76
|
+
ensure
|
77
|
+
@buffer = {}
|
78
|
+
sorted_keys.each do |key|
|
79
|
+
@lock.release_lock(key)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def respond_to?(method)
|
84
|
+
@cache.respond_to?(method)
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def perform_commands
|
90
|
+
@commands.each do |command|
|
91
|
+
command.call(@cache)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def buffer_command(command)
|
96
|
+
@commands << command
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def method_missing(method, *args, &block)
|
102
|
+
@cache.send(method, *args, &block)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class NestedBuffered < Buffered
|
107
|
+
def flush
|
108
|
+
perform_commands
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Command
|
113
|
+
attr_accessor :key
|
114
|
+
|
115
|
+
def initialize(name, key, *args)
|
116
|
+
@name = name
|
117
|
+
@key = key
|
118
|
+
@args = args
|
119
|
+
end
|
120
|
+
|
121
|
+
def requires_lock?
|
122
|
+
@name == :set
|
123
|
+
end
|
124
|
+
|
125
|
+
def call(cache)
|
126
|
+
cache.send @name, @key, *@args
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/cash/config.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Cash
|
2
|
+
module Config
|
3
|
+
def self.create(active_record, options, indices = [])
|
4
|
+
active_record.cache_config = Cash::Config::Config.new(active_record, options)
|
5
|
+
indices.each { |i| active_record.index i.attributes, i.options }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(a_module)
|
9
|
+
a_module.module_eval do
|
10
|
+
extend ClassMethods
|
11
|
+
delegate :repository, :to => "self.class"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def self.extended(a_class)
|
17
|
+
class << a_class
|
18
|
+
def cache_config
|
19
|
+
@cache_config ? @cache_config : superclass.cache_config
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate :repository, :indices, :to => :cache_config
|
23
|
+
alias_method_chain :inherited, :cache_config
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def inherited_with_cache_config(subclass)
|
28
|
+
inherited_without_cache_config(subclass)
|
29
|
+
@cache_config.inherit(subclass)
|
30
|
+
end
|
31
|
+
|
32
|
+
def index(attributes, options = {})
|
33
|
+
options.assert_valid_keys(:ttl, :order, :limit, :buffer, :order_column)
|
34
|
+
(@cache_config.indices.unshift(Index.new(@cache_config, self, attributes, options))).uniq!
|
35
|
+
end
|
36
|
+
|
37
|
+
def version(number)
|
38
|
+
@cache_config.options[:version] = number
|
39
|
+
end
|
40
|
+
|
41
|
+
def cache_config=(config)
|
42
|
+
@cache_config = config
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Config
|
47
|
+
attr_reader :active_record, :options
|
48
|
+
|
49
|
+
def initialize(active_record, options = {})
|
50
|
+
@active_record, @options = active_record, options
|
51
|
+
end
|
52
|
+
|
53
|
+
def repository
|
54
|
+
@options[:repository]
|
55
|
+
end
|
56
|
+
|
57
|
+
def ttl
|
58
|
+
@ttl ||= @options[:ttl] || default_ttl
|
59
|
+
end
|
60
|
+
|
61
|
+
def version
|
62
|
+
@options[:version] || 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def indices
|
66
|
+
@indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
|
67
|
+
end
|
68
|
+
|
69
|
+
def inherit(active_record)
|
70
|
+
Cash::Config.create(active_record, @options, indices)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def default_ttl
|
76
|
+
default = repository.default_ttl if repository.respond_to?(:default_ttl)
|
77
|
+
default ||= 1.day
|
78
|
+
default
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|