record-cache 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.
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