record-cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/lib/record-cache.rb +1 -0
  2. data/lib/record_cache/active_record.rb +318 -0
  3. data/lib/record_cache/base.rb +136 -0
  4. data/lib/record_cache/dispatcher.rb +90 -0
  5. data/lib/record_cache/multi_read.rb +51 -0
  6. data/lib/record_cache/query.rb +85 -0
  7. data/lib/record_cache/statistics.rb +82 -0
  8. data/lib/record_cache/strategy/base.rb +154 -0
  9. data/lib/record_cache/strategy/id_cache.rb +93 -0
  10. data/lib/record_cache/strategy/index_cache.rb +122 -0
  11. data/lib/record_cache/strategy/request_cache.rb +49 -0
  12. data/lib/record_cache/test/resettable_version_store.rb +49 -0
  13. data/lib/record_cache/version.rb +5 -0
  14. data/lib/record_cache/version_store.rb +54 -0
  15. data/lib/record_cache.rb +11 -0
  16. data/spec/db/database.yml +6 -0
  17. data/spec/db/schema.rb +42 -0
  18. data/spec/db/seeds.rb +40 -0
  19. data/spec/initializers/record_cache.rb +14 -0
  20. data/spec/lib/dispatcher_spec.rb +86 -0
  21. data/spec/lib/multi_read_spec.rb +51 -0
  22. data/spec/lib/query_spec.rb +148 -0
  23. data/spec/lib/statistics_spec.rb +140 -0
  24. data/spec/lib/strategy/base_spec.rb +241 -0
  25. data/spec/lib/strategy/id_cache_spec.rb +168 -0
  26. data/spec/lib/strategy/index_cache_spec.rb +223 -0
  27. data/spec/lib/strategy/request_cache_spec.rb +85 -0
  28. data/spec/lib/version_store_spec.rb +104 -0
  29. data/spec/models/apple.rb +8 -0
  30. data/spec/models/banana.rb +8 -0
  31. data/spec/models/pear.rb +6 -0
  32. data/spec/models/person.rb +11 -0
  33. data/spec/models/store.rb +13 -0
  34. data/spec/spec_helper.rb +44 -0
  35. data/spec/support/after_commit.rb +71 -0
  36. data/spec/support/matchers/hit_cache_matcher.rb +53 -0
  37. data/spec/support/matchers/miss_cache_matcher.rb +53 -0
  38. data/spec/support/matchers/use_cache_matcher.rb +53 -0
  39. metadata +253 -0
@@ -0,0 +1,82 @@
1
+ module RecordCache
2
+
3
+ # Collect cache hit/miss statistics for each cache strategy
4
+ module Statistics
5
+
6
+ class << self
7
+
8
+ # returns +true+ if statistics need to be collected
9
+ def active?
10
+ !!@active
11
+ end
12
+
13
+ # start statistics collection
14
+ def start
15
+ @active = true
16
+ end
17
+
18
+ # stop statistics collection
19
+ def stop
20
+ @active = false
21
+ end
22
+
23
+ # toggle statistics collection
24
+ def toggle
25
+ @active = !@active
26
+ end
27
+
28
+ # reset all statistics
29
+ def reset!(base = nil)
30
+ stats = find(base).values
31
+ stats = stats.map(&:values).flatten unless base # flatten hash of hashes in case base was nil
32
+ stats.each{ |s| s.reset! }
33
+ end
34
+
35
+ # Retrieve the statistics for the given base and strategy_id
36
+ # Returns a hash {<stategy_id> => <statistics} for a model if no strategy is provided
37
+ # Returns a hash of hashes { <model_name> => {<stategy_id> => <statistics} } if no parameter is provided
38
+ def find(base = nil, strategy_id = nil)
39
+ stats = (@stats ||= {})
40
+ stats = (stats[base.name] ||= {}) if base
41
+ stats = (stats[strategy_id] ||= Counter.new) if strategy_id
42
+ stats
43
+ end
44
+ end
45
+
46
+ class Counter
47
+ attr_accessor :calls, :hits, :misses
48
+
49
+ def initialize
50
+ reset!
51
+ end
52
+
53
+ # add hit statatistics for the given cache strategy
54
+ # @param queried: nr of ids queried
55
+ # @param found: nr of records found in the cache
56
+ def add(queried, found)
57
+ @calls += 1
58
+ @hits += found
59
+ @misses += (queried - found)
60
+ end
61
+
62
+ def reset!
63
+ @hits = 0
64
+ @misses = 0
65
+ @calls = 0
66
+ end
67
+
68
+ def active?
69
+ RecordCache::Statistics.active?
70
+ end
71
+
72
+ def percentage
73
+ return 0.0 if @hits == 0
74
+ (@hits.to_f / (@hits + @misses)) * 100
75
+ end
76
+
77
+ def inspect
78
+ "#{percentage}% (#{@hits}/#{@hits + @misses})"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,154 @@
1
+ module RecordCache
2
+ module Strategy
3
+ class Base
4
+ CLASS_KEY = :c
5
+ ATTRIBUTES_KEY = :a
6
+
7
+ def initialize(base, strategy_id, record_store, options)
8
+ @base = base
9
+ @strategy_id = strategy_id
10
+ @record_store = record_store
11
+ @cache_key_prefix = "rc/#{options[:key] || @base.name}/".freeze
12
+ end
13
+
14
+ # Fetch all records and sort and filter locally
15
+ def fetch(query)
16
+ records = fetch_records(query)
17
+ filter!(records, query.wheres) if query.wheres.size > 0
18
+ sort!(records, query.sort_orders) if query.sorted?
19
+ records
20
+ end
21
+
22
+ # Handle create/update/destroy (use record.previous_changes to find the old values in case of an update)
23
+ def record_change(record, action)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # Can the cache retrieve the records based on this query?
28
+ def cacheable?(query)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # Handle invalidation call
33
+ def invalidate(id)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ protected
38
+
39
+ def fetch_records(query)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # ------------------------- Utility methods ----------------------------
44
+
45
+ # retrieve the version store (unique store for the whole application)
46
+ def version_store
47
+ RecordCache::Base.version_store
48
+ end
49
+
50
+ # retrieve the record store (store for records for this cache strategy)
51
+ def record_store
52
+ @record_store
53
+ end
54
+
55
+ # find the statistics for this cache strategy
56
+ def statistics
57
+ @statistics ||= RecordCache::Statistics.find(@base, @strategy_id)
58
+ end
59
+
60
+ # retrieve the cache key for the given id, e.g. rc/person/14
61
+ def cache_key(id)
62
+ "#{@cache_key_prefix}#{id}".freeze
63
+ end
64
+
65
+ # retrieve the versioned record key, e.g. rc/person/14v1
66
+ def versioned_key(cache_key, version)
67
+ "#{cache_key}v#{version.to_s}".freeze
68
+ end
69
+
70
+ # serialize one record before adding it to the cache
71
+ # creates a shallow clone with a version and without associations
72
+ def serialize(record)
73
+ {CLASS_KEY => record.class.name,
74
+ ATTRIBUTES_KEY => record.instance_variable_get(:@attributes)}.freeze
75
+ end
76
+
77
+ # deserialize a cached record
78
+ def deserialize(serialized)
79
+ record = serialized[CLASS_KEY].constantize.new
80
+ attributes = serialized[ATTRIBUTES_KEY]
81
+ record.instance_variable_set(:@attributes, Hash[attributes])
82
+ record.instance_variable_set(:@new_record, false)
83
+ record.instance_variable_set(:@changed_attributes, {})
84
+ record.instance_variable_set(:@previously_changed, {})
85
+ record
86
+ end
87
+
88
+ private
89
+
90
+ # Filter the cached records in memory
91
+ # only simple x = y or x IN (a,b,c) can be handled
92
+ def filter!(records, wheres)
93
+ wheres.each_pair do |attr, value|
94
+ if value.is_a?(Array)
95
+ records.reject! { |record| !value.include?(record.send(attr)) }
96
+ else
97
+ records.reject! { |record| record.send(attr) != value }
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sort the cached records in memory
103
+ def sort!(records, sort_orders)
104
+ records.sort!(&sort_proc(sort_orders))
105
+ Collator.clear
106
+ records
107
+ end
108
+
109
+ # Retrieve the Proc based on the order by attributes
110
+ # Note: Case insensitive sorting with collation is used for Strings
111
+ def sort_proc(sort_orders)
112
+ # [['(COLLATER.collate(x.name) || NIL_COMES_FIRST)', 'COLLATER.collate(y.name)'], ['(y.updated_at || NIL_COMES_FIRST)', 'x.updated_at']]
113
+ sort = sort_orders.map do |attr, asc|
114
+ lr = ["x.", "y."]
115
+ lr.reverse! unless asc
116
+ lr.each{ |s| s << attr }
117
+ lr.each{ |s| s.replace("Collator.collate(#{s})") } if @base.columns_hash[attr].type == :string
118
+ lr[0].replace("(#{lr[0]} || NIL_COMES_FIRST)")
119
+ lr
120
+ end
121
+ # ['[(COLLATER.collate(x.name) || NIL_COMES_FIRST), (y.updated_at || NIL_COMES_FIRST)]', '[COLLATER.collate(y.name), x.updated_at]']
122
+ sort = sort.transpose.map{|s| s.size == 1 ? s.first : "[#{s.join(',')}]"}
123
+ # Proc.new{ |x,y| { ([(COLLATER.collate(x.name) || NIL_COMES_FIRST), (y.updated_at || NIL_COMES_FIRST)] <=> [COLLATER.collate(y.name), x.updated_at]) || 1 }
124
+ eval("Proc.new{ |x,y| (#{sort[0]} <=> #{sort[1]}) || 1 }")
125
+ end
126
+
127
+ # If +x.nil?+ this class will return -1 for +x <=> y+
128
+ NIL_COMES_FIRST = ((class NilComesFirst; def <=>(y); -1; end; end); NilComesFirst.new)
129
+
130
+ # StringCollator uses the Rails transliterate method for collation
131
+ module Collator
132
+ @collated = []
133
+
134
+ def self.clear
135
+ @collated.each { |string| string.send(:remove_instance_variable, :@rc_collated) }
136
+ @collated.clear
137
+ end
138
+
139
+ def self.collate(string)
140
+ collated = string.instance_variable_get(:@rc_collated)
141
+ return collated if collated
142
+ normalized = ActiveSupport::Multibyte::Unicode.normalize(ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c).mb_chars
143
+ collated = I18n.transliterate(normalized).downcase.mb_chars
144
+ # transliterate will replace ignored/unknown chars with ? the following line replaces ? with the original character
145
+ collated.chars.each_with_index{ |c, i| collated[i] = normalized[i] if c == '?' } if collated.index('?')
146
+ # puts "collation: #{string} => #{collated.to_s}"
147
+ string.instance_variable_set(:@rc_collated, collated)
148
+ @collated << string
149
+ collated
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,93 @@
1
+ module RecordCache
2
+ module Strategy
3
+ class IdCache < Base
4
+
5
+ # Can the cache retrieve the records based on this query?
6
+ def cacheable?(query)
7
+ ids = query.where_ids(:id)
8
+ ids && (query.limit.nil? || (query.limit == 1 && ids.size == 1))
9
+ end
10
+
11
+ # Update the version store and the record store
12
+ def record_change(record, action)
13
+ key = cache_key(record.id)
14
+ if action == :destroy
15
+ version_store.delete(key)
16
+ else
17
+ # update the version store and add the record to the cache
18
+ new_version = version_store.increment(key)
19
+ record_store.write(versioned_key(key, new_version), serialize(record))
20
+ end
21
+ end
22
+
23
+ # Handle invalidation call
24
+ def invalidate(id)
25
+ version_store.delete(cache_key(id))
26
+ end
27
+
28
+ protected
29
+
30
+ # retrieve the record(s) with the given id(s) as an array
31
+ def fetch_records(query)
32
+ ids = query.where_ids(:id)
33
+ query.wheres.delete(:id) # make sure CacheCase.filter! does not see this where anymore
34
+ id_to_key_map = ids.inject({}){|h,id| h[id] = cache_key(id); h }
35
+ # retrieve the current version of the records
36
+ current_versions = version_store.current_multi(id_to_key_map)
37
+ # get the keys for the records for which a current version was found
38
+ id_to_version_key_map = Hash[id_to_key_map.map{ |id, key| current_versions[id] ? [id, versioned_key(key, current_versions[id])] : nil }]
39
+ # retrieve the records from the cache
40
+ records = id_to_version_key_map.size > 0 ? from_cache(id_to_version_key_map) : []
41
+ # query the records with missing ids
42
+ id_to_key_map.except!(*records.map(&:id))
43
+ # logging (only in debug mode!) and statistics
44
+ log_id_cache_hit(ids, id_to_key_map.keys) if RecordCache::Base.logger.debug?
45
+ statistics.add(ids.size, records.size) if statistics.active?
46
+ # retrieve records from DB in case there are some missing ids
47
+ records += from_db(id_to_key_map, id_to_version_key_map) if id_to_key_map.size > 0
48
+ # return the array
49
+ records
50
+ end
51
+
52
+ private
53
+
54
+ # ---------------------------- Querying ------------------------------------
55
+
56
+ # retrieve the records from the cache with the given keys
57
+ def from_cache(id_to_versioned_key_map)
58
+ records = record_store.read_multi(*(id_to_versioned_key_map.values)).values.compact
59
+ records.map{ |record| deserialize(record) }
60
+ end
61
+
62
+ # retrieve the records with the given ids from the database
63
+ def from_db(id_to_key_map, id_to_version_key_map)
64
+ RecordCache::Base.without_record_cache do
65
+ # retrieve the records from the database
66
+ records = @base.where(:id => id_to_key_map.keys).to_a
67
+ records.each do |record|
68
+ versioned_key = id_to_version_key_map[record.id]
69
+ unless versioned_key
70
+ # renew the key in the version store in case it was missing
71
+ key = id_to_key_map[record.id]
72
+ versioned_key = versioned_key(key, version_store.renew(key))
73
+ end
74
+ # store the record based on the versioned key
75
+ record_store.write(versioned_key, serialize(record))
76
+ end
77
+ records
78
+ end
79
+ end
80
+
81
+ # ------------------------- Utility methods ----------------------------
82
+
83
+ # log cache hit/miss to debug log
84
+ def log_id_cache_hit(ids, missing_ids)
85
+ hit = missing_ids.empty? ? "hit" : ids.size == missing_ids.size ? "miss" : "partial hit"
86
+ missing = missing_ids.empty? || ids.size == missing_ids.size ? "" : ": missing #{missing_ids.inspect}"
87
+ msg = "IdCache #{hit} for ids #{ids.size == 1 ? ids.first : ids.inspect}#{missing}"
88
+ RecordCache::Base.logger.debug(msg)
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,122 @@
1
+ module RecordCache
2
+ module Strategy
3
+ class IndexCache < Base
4
+
5
+ def initialize(base, strategy_id, record_store, options)
6
+ super
7
+ @index = options[:index]
8
+ # check the index
9
+ type = @base.columns_hash[@index.to_s].try(:type)
10
+ raise "No column found for index '#{@index}' on #{@base.name}." unless type
11
+ raise "Incorrect type (expected integer, found #{type}) for index '#{@index}' on #{@base.name}." unless type == :integer
12
+ @index_cache_key_prefix = cache_key(@index) # "/rc/<model>/<index>"
13
+ end
14
+
15
+ # Can the cache retrieve the records based on this query?
16
+ def cacheable?(query)
17
+ query.where_id(@index) && query.limit.nil?
18
+ end
19
+
20
+ # Handle create/update/destroy (use record.previous_changes to find the old values in case of an update)
21
+ def record_change(record, action)
22
+ if action == :destroy
23
+ remove_from_index(record.send(@index), record.id)
24
+ elsif action == :create
25
+ add_to_index(record.send(@index), record.id)
26
+ else
27
+ index_change = record.previous_changes[@index.to_s]
28
+ return unless index_change
29
+ remove_from_index(index_change[0], record.id)
30
+ add_to_index(index_change[1], record.id)
31
+ end
32
+ end
33
+
34
+ # Explicitly invalidate the record cache for the given value
35
+ def invalidate(value)
36
+ version_store.increment(index_cache_key(value))
37
+ end
38
+
39
+ protected
40
+
41
+ # retrieve the record(s) based on the given query
42
+ def fetch_records(query)
43
+ value = query.where_id(@index)
44
+ # make sure CacheCase.filter! does not see this where clause anymore
45
+ query.wheres.delete(@index)
46
+ # retrieve the cache key for this index and value
47
+ key = index_cache_key(value)
48
+ # retrieve the current version of the ids list
49
+ current_version = version_store.current(key)
50
+ # create the versioned key, renew the version in case it was missing in the version store
51
+ versioned_key = versioned_key(key, current_version || version_store.renew(key))
52
+ # retrieve the ids from the local cache based on the current version from the version store
53
+ ids = current_version ? fetch_ids_from_cache(versioned_key) : nil
54
+ # logging (only in debug mode!) and statistics
55
+ log_cache_hit(versioned_key, ids) if RecordCache::Base.logger.debug?
56
+ statistics.add(1, ids ? 1 : 0) if statistics.active?
57
+ # retrieve the ids from the DB if the result was not fresh
58
+ ids = fetch_ids_from_db(versioned_key, value) unless ids
59
+ # use the IdCache to retrieve the records based on the ids
60
+ @base.record_cache[:id].send(:fetch_records, ::RecordCache::Query.new({:id => ids}))
61
+ end
62
+
63
+ private
64
+
65
+ # ---------------------------- Querying ------------------------------------
66
+
67
+ # key to retrieve the ids for a given value
68
+ def index_cache_key(value)
69
+ "#{@index_cache_key_prefix}=#{value}"
70
+ end
71
+
72
+ # Retrieve the ids from the local cache
73
+ def fetch_ids_from_cache(versioned_key)
74
+ record_store.read(versioned_key)
75
+ end
76
+
77
+ # retrieve the ids from the database and update the local cache
78
+ def fetch_ids_from_db(versioned_key, value)
79
+ RecordCache::Base.without_record_cache do
80
+ # go straight to SQL result for optimal performance
81
+ sql = @base.select('id').where(@index => value).to_sql
82
+ ids = []; @base.connection.execute(sql).each{ |row| ids << (row.is_a?(Hash) ? row['id'] : row.first).to_i }
83
+ record_store.write(versioned_key, ids)
84
+ ids
85
+ end
86
+ end
87
+
88
+ # ---------------------------- Local Record Changes ---------------------------------
89
+
90
+ # add one record(id) to the index with the given value
91
+ def add_to_index(value, id)
92
+ increment_version(value.to_i) { |ids| ids << id } if value
93
+ end
94
+
95
+ # remove one record(id) from the index with the given value
96
+ def remove_from_index(value, id)
97
+ increment_version(value.to_i) { |ids| ids.delete(id) } if value
98
+ end
99
+
100
+ # increment the version store and update the local store
101
+ def increment_version(value, &block)
102
+ # retrieve local version and increment version store
103
+ key = index_cache_key(value)
104
+ version = version_store.increment(key)
105
+ # try to update the ids list based on the last version
106
+ ids = fetch_ids_from_cache(versioned_key(key, version - 1))
107
+ if ids
108
+ ids = Array.new(ids)
109
+ yield ids
110
+ record_store.write(versioned_key(key, version), ids)
111
+ end
112
+ end
113
+
114
+ # ------------------------- Utility methods ----------------------------
115
+
116
+ # log cache hit/miss to debug log
117
+ def log_cache_hit(key, ids)
118
+ RecordCache::Base.logger.debug("IndexCache #{ids ? 'hit' : 'miss'} for #{key}: found #{ids ? ids.size : 'no'} ids")
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,49 @@
1
+ # Remembers the queries performed during a single Request.
2
+ # If the same query is requested again the result is provided straight from local memory.
3
+ #
4
+ # Records are invalidated per model-klass, when any record is created, updated or destroyed.
5
+ module RecordCache
6
+ module Strategy
7
+
8
+ class RequestCache < Base
9
+ @@request_store = {}
10
+
11
+ # call before each request: in application_controller.rb
12
+ # before_filter { |c| RecordCache::Strategy::RequestCache.clear }
13
+ def self.clear
14
+ @@request_store.clear
15
+ end
16
+
17
+ # Handle record change
18
+ def record_change(record, action)
19
+ @@request_store.delete(@base.name)
20
+ end
21
+
22
+ # Handle invalidation call
23
+ def invalidate(value)
24
+ @@request_store.delete(@base.name)
25
+ end
26
+
27
+ # return the records from the request cache, execute block in case
28
+ # this is the first time this query is performed during this request
29
+ def fetch(query, &block)
30
+ klass_store = (@@request_store[@base.name] ||= {})
31
+ key = query.cache_key
32
+ # logging (only in debug mode!) and statistics
33
+ log_cache_hit(key, klass_store.key?(key)) if RecordCache::Base.logger.debug?
34
+ statistics.add(1, klass_store.key?(key) ? 1 : 0) if statistics.active?
35
+ klass_store[key] ||= yield
36
+ end
37
+
38
+ private
39
+
40
+ # ------------------------- Utility methods ----------------------------
41
+
42
+ # log cache hit/miss to debug log
43
+ def log_cache_hit(key, hit)
44
+ RecordCache::Base.logger.debug("RequestCache #{hit ? 'hit' : 'miss'} for #{key}")
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # Make sure the version store can be reset to it's starting point after each test
2
+ # Usage:
3
+ # require 'record_cache/test/resettable_version_store'
4
+ # after(:each) { RecordCache::Base.version_store.reset! }
5
+ module RecordCache
6
+ module Test
7
+
8
+ module ResettableVersionStore
9
+
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ base.send(:include, InstanceMethods)
13
+ base.instance_eval do
14
+ alias_method_chain :increment, :reset
15
+ alias_method_chain :renew, :reset
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ end
21
+
22
+ module InstanceMethods
23
+
24
+ def increment_with_reset(key)
25
+ updated_version_keys << key
26
+ increment_without_reset(key)
27
+ end
28
+
29
+ def renew_with_reset(key)
30
+ updated_version_keys << key
31
+ renew_without_reset(key)
32
+ end
33
+
34
+ def reset!
35
+ RecordCache::Strategy::RequestCache.clear
36
+ updated_version_keys.each { |key| delete(key) }
37
+ updated_version_keys.clear
38
+ end
39
+
40
+ def updated_version_keys
41
+ @updated_version_keys ||= []
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+ RecordCache::VersionStore.send(:include, RecordCache::Test::ResettableVersionStore)
@@ -0,0 +1,5 @@
1
+ module RecordCache # :nodoc:
2
+ module Version # :nodoc:
3
+ STRING = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ module RecordCache
2
+ class VersionStore
3
+ attr_accessor :store
4
+
5
+ def initialize(store)
6
+ raise "Must be an ActiveSupport::Cache::Store" unless store.is_a?(ActiveSupport::Cache::Store)
7
+ @store = store
8
+ end
9
+
10
+ # Retrieve the current versions for the given key
11
+ # @return nil in case the key is not known in the version store
12
+ def current(key)
13
+ @store.read(key)
14
+ end
15
+
16
+ # Retrieve the current versions for the given keys
17
+ # @param id_key_map is a map with {id => cache_key}
18
+ # @return a map with {id => current_version}
19
+ # version nil for all keys unknown to the version store
20
+ def current_multi(id_key_map)
21
+ current_versions = @store.read_multi(*(id_key_map.values))
22
+ Hash[id_key_map.map{ |id, key| [id, current_versions[key]] }]
23
+ end
24
+
25
+ # In case the version store did not have a key anymore, call this methods
26
+ # to reset the key with a unique new key
27
+ def renew(key)
28
+ new_version = (Time.current.to_f * 10000).to_i
29
+ @store.write(key, new_version)
30
+ RecordCache::Base.logger.debug("Version Store: renew #{key}: nil => #{new_version}") if RecordCache::Base.logger.debug?
31
+ new_version
32
+ end
33
+
34
+ # Increment the current version for the given key, in case of record updates
35
+ def increment(key)
36
+ version = @store.increment(key, 1)
37
+ # renew key in case the version store already purged the key
38
+ if version.nil? || version == 1
39
+ version = renew(key)
40
+ else
41
+ RecordCache::Base.logger.debug("Version Store: incremented #{key}: #{version - 1} => #{version}") if RecordCache::Base.logger.debug?
42
+ end
43
+ version
44
+ end
45
+
46
+ # Delete key from the version store, in case the record(s) are destroyed
47
+ def delete(key)
48
+ deleted = @store.delete(key)
49
+ RecordCache::Base.logger.debug("Version Store: deleted #{key}") if RecordCache::Base.logger.debug?
50
+ deleted
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ # Record Cache shared files
2
+ ["query", "version_store", "multi_read",
3
+ "strategy/base", "strategy/id_cache", "strategy/index_cache", "strategy/request_cache",
4
+ "statistics", "dispatcher", "base"].each do |file|
5
+ require File.dirname(__FILE__) + "/record_cache/#{file}.rb"
6
+ end
7
+
8
+ # Support for Active Record
9
+ require 'active_record'
10
+ ActiveRecord::Base.send(:include, RecordCache::Base)
11
+ require File.dirname(__FILE__) + "/record_cache/active_record.rb"
@@ -0,0 +1,6 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
4
+ encoding: utf8
5
+ charset: utf8
6
+ timeout: 5000
data/spec/db/schema.rb ADDED
@@ -0,0 +1,42 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+
3
+ create_table :people, :force => true do |t|
4
+ t.integer :id
5
+ t.string :name
6
+ t.date :birthday
7
+ t.float :height
8
+ end
9
+
10
+ create_table :stores, :force => true do |t|
11
+ t.integer :id
12
+ t.string :name
13
+ t.integer :owner_id
14
+ end
15
+
16
+ create_table :people_stores, :id => false, :force => true do |t|
17
+ t.integer :person_id
18
+ t.string :store_id
19
+ end
20
+
21
+ create_table :apples, :force => true do |t|
22
+ t.integer :id
23
+ t.string :name
24
+ t.integer :store_id
25
+ t.integer :person_id
26
+ end
27
+
28
+ create_table :bananas, :force => true do |t|
29
+ t.integer :id
30
+ t.string :name
31
+ t.integer :store_id
32
+ t.integer :person_id
33
+ end
34
+
35
+ create_table :pears, :force => true do |t|
36
+ t.integer :id
37
+ t.string :name
38
+ t.integer :store_id
39
+ t.integer :person_id
40
+ end
41
+
42
+ end