identity_cache 0.5.1 → 1.0.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 (91) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +24 -9
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +5 -1
  9. data/README.md +28 -26
  10. data/Rakefile +14 -5
  11. data/dev.yml +9 -16
  12. data/gemfiles/Gemfile.latest-release +6 -0
  13. data/gemfiles/Gemfile.rails-edge +6 -0
  14. data/gemfiles/Gemfile.rails52 +6 -0
  15. data/identity_cache.gemspec +26 -10
  16. data/lib/identity_cache.rb +49 -46
  17. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  18. data/lib/identity_cache/cache_fetcher.rb +6 -5
  19. data/lib/identity_cache/cache_hash.rb +2 -2
  20. data/lib/identity_cache/cache_invalidation.rb +4 -11
  21. data/lib/identity_cache/cache_key_generation.rb +17 -65
  22. data/lib/identity_cache/cache_key_loader.rb +128 -0
  23. data/lib/identity_cache/cached.rb +7 -0
  24. data/lib/identity_cache/cached/association.rb +87 -0
  25. data/lib/identity_cache/cached/attribute.rb +123 -0
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  27. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  28. data/lib/identity_cache/cached/belongs_to.rb +93 -0
  29. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  30. data/lib/identity_cache/cached/prefetcher.rb +51 -0
  31. data/lib/identity_cache/cached/primary_index.rb +97 -0
  32. data/lib/identity_cache/cached/recursive/association.rb +68 -0
  33. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  34. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  35. data/lib/identity_cache/cached/reference/association.rb +16 -0
  36. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  37. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  38. data/lib/identity_cache/configuration_dsl.rb +53 -215
  39. data/lib/identity_cache/encoder.rb +95 -0
  40. data/lib/identity_cache/expiry_hook.rb +36 -0
  41. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  42. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  43. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  44. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  45. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  46. data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
  47. data/lib/identity_cache/parent_model_expiration.rb +45 -11
  48. data/lib/identity_cache/query_api.rb +128 -394
  49. data/lib/identity_cache/railtie.rb +8 -0
  50. data/lib/identity_cache/record_not_found.rb +6 -0
  51. data/lib/identity_cache/should_use_cache.rb +1 -0
  52. data/lib/identity_cache/version.rb +3 -2
  53. data/lib/identity_cache/with_primary_index.rb +136 -0
  54. data/lib/identity_cache/without_primary_index.rb +24 -3
  55. data/performance/cache_runner.rb +28 -34
  56. data/performance/cpu.rb +3 -2
  57. data/performance/externals.rb +4 -3
  58. data/performance/profile.rb +6 -5
  59. data/railgun.yml +16 -0
  60. metadata +44 -73
  61. data/Gemfile.rails42 +0 -6
  62. data/Gemfile.rails50 +0 -6
  63. data/test/attribute_cache_test.rb +0 -110
  64. data/test/cache_fetch_includes_test.rb +0 -46
  65. data/test/cache_hash_test.rb +0 -14
  66. data/test/cache_invalidation_test.rb +0 -139
  67. data/test/deeply_nested_associated_record_test.rb +0 -19
  68. data/test/denormalized_has_many_test.rb +0 -214
  69. data/test/denormalized_has_one_test.rb +0 -160
  70. data/test/fetch_multi_test.rb +0 -308
  71. data/test/fetch_test.rb +0 -258
  72. data/test/fixtures/serialized_record.mysql2 +0 -0
  73. data/test/fixtures/serialized_record.postgresql +0 -0
  74. data/test/helpers/active_record_objects.rb +0 -106
  75. data/test/helpers/database_connection.rb +0 -72
  76. data/test/helpers/serialization_format.rb +0 -51
  77. data/test/helpers/update_serialization_format.rb +0 -27
  78. data/test/identity_cache_test.rb +0 -29
  79. data/test/index_cache_test.rb +0 -161
  80. data/test/memoized_attributes_test.rb +0 -59
  81. data/test/memoized_cache_proxy_test.rb +0 -107
  82. data/test/normalized_belongs_to_test.rb +0 -107
  83. data/test/normalized_has_many_test.rb +0 -231
  84. data/test/normalized_has_one_test.rb +0 -9
  85. data/test/prefetch_associations_test.rb +0 -379
  86. data/test/readonly_test.rb +0 -109
  87. data/test/recursive_denormalized_has_many_test.rb +0 -131
  88. data/test/save_test.rb +0 -82
  89. data/test/schema_change_test.rb +0 -112
  90. data/test/serialization_format_change_test.rb +0 -16
  91. data/test/test_helper.rb +0 -140
@@ -1,109 +0,0 @@
1
- require "test_helper"
2
-
3
- class ReadonlyTest < IdentityCache::TestCase
4
- def setup
5
- super
6
- @key, @value = 'foo', 'bar'
7
- @record = Item.new
8
- @record.id = 1
9
- @record.title = 'bob'
10
- @bob = Item.create!(:title => 'bob')
11
- @joe = Item.create!(:title => 'joe')
12
- @fred = Item.create!(:title => 'fred')
13
- IdentityCache.cache.clear
14
- IdentityCache.readonly = true
15
- end
16
-
17
- def teardown
18
- IdentityCache.readonly = nil
19
- super
20
- end
21
-
22
- def test_write_should_not_update_cache
23
- assert_memcache_operations(0) do
24
- fetcher.write(@key, @value)
25
- end
26
- assert_nil backend.read(@key)
27
- end
28
-
29
- def test_delete_should_update_cache
30
- backend.write(@key, @value)
31
- fetcher.delete(@key)
32
- assert_equal deleted_value, backend.read(@key)
33
- end
34
-
35
- def test_clear_should_update_cache
36
- backend.write(@key, @value)
37
- fetcher.clear
38
- assert_nil backend.read(@key)
39
- end
40
-
41
- def test_fetch_should_not_update_cache
42
- fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
43
- Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
44
-
45
- assert_readonly_fetch do
46
- assert_equal @record, Item.fetch(1)
47
- end
48
- assert_nil backend.read(@record.primary_cache_index_key)
49
- assert fetch.has_been_called_with?(@record.primary_cache_index_key)
50
- end
51
-
52
- def test_fetch_multi_should_not_update_cache
53
- fetch_multi = Spy.on(IdentityCache.cache, :fetch_multi).and_call_through
54
-
55
- assert_readonly_fetch_multi do
56
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
57
- end
58
- keys = [@bob, @joe, @fred].map(&:primary_cache_index_key)
59
- assert_empty backend.read_multi(*keys)
60
- assert fetch_multi.has_been_called_with?(*keys)
61
- end
62
-
63
- protected
64
-
65
- def assert_readonly_fetch
66
- cas = Spy.on(backend, :cas).and_call_through
67
- yield
68
- assert cas.has_been_called?
69
- end
70
-
71
- def assert_readonly_fetch_multi
72
- cas_multi = Spy.on(backend, :cas_multi).and_call_through
73
- yield
74
- assert cas_multi.has_been_called?
75
- end
76
-
77
- def deleted_value
78
- IdentityCache::DELETED
79
- end
80
- end
81
-
82
- class FallbackReadonlyTest < ReadonlyTest
83
- def setup
84
- super
85
- IdentityCache.cache_backend = @backend = ActiveSupport::Cache::MemoryStore.new
86
- end
87
-
88
- protected
89
-
90
- def assert_readonly_fetch
91
- read = Spy.on(backend, :read).and_call_through
92
- write = Spy.on(backend, :write).and_call_through
93
- yield
94
- assert read.has_been_called?
95
- refute write.has_been_called?
96
- end
97
-
98
- def assert_readonly_fetch_multi
99
- read_multi = Spy.on(backend, :read_multi).and_call_through
100
- write = Spy.on(backend, :write).and_call_through
101
- yield
102
- assert read_multi.has_been_called?
103
- refute write.has_been_called?
104
- end
105
-
106
- def deleted_value
107
- nil
108
- end
109
- end
@@ -1,131 +0,0 @@
1
- require "test_helper"
2
-
3
- class RecursiveDenormalizedHasManyTest < IdentityCache::TestCase
4
- def setup
5
- super
6
- AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
7
- Item.cache_has_many :associated_records, :embed => true
8
- Item.cache_has_one :associated
9
-
10
- @record = Item.new(:title => 'foo')
11
-
12
- @associated_record = AssociatedRecord.new(:name => 'bar')
13
- @record.associated_records << AssociatedRecord.new(:name => 'baz')
14
- @record.associated_records << @associated_record
15
-
16
- @deeply_associated_record = DeeplyAssociatedRecord.new(:name => "corge")
17
- @associated_record.deeply_associated_records << @deeply_associated_record
18
- @associated_record.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux")
19
-
20
- @record.save
21
- @record.reload
22
- @associated_record.reload
23
- end
24
-
25
- def test_cache_fetch_includes
26
- assert_equal [{:associated_records => [:deeply_associated_records]}, :associated => [:deeply_associated_records]], Item.send(:cache_fetch_includes)
27
- end
28
-
29
- def test_uncached_record_from_the_db_will_use_normal_association_for_deeply_associated_records
30
- expected = @associated_record.deeply_associated_records
31
- record_from_db = Item.find(@record.id)
32
- assert_equal expected, record_from_db.fetch_associated_records[0].fetch_deeply_associated_records
33
- end
34
-
35
- def test_on_cache_miss_record_should_embed_associated_objects_and_return
36
- record_from_cache_miss = Item.fetch(@record.id)
37
- expected = @associated_record.deeply_associated_records
38
-
39
- child_record_from_cache_miss = record_from_cache_miss.fetch_associated_records[0]
40
- assert_equal @associated_record, child_record_from_cache_miss
41
- assert_equal expected, child_record_from_cache_miss.fetch_deeply_associated_records
42
- end
43
-
44
- def test_on_cache_hit_record_should_return_embed_associated_objects
45
- Item.fetch(@record.id) # warm cache
46
- expected = @associated_record.deeply_associated_records
47
-
48
- Item.any_instance.expects(:associated_records).never
49
- AssociatedRecord.any_instance.expects(:deeply_associated_records).never
50
-
51
- record_from_cache_hit = Item.fetch(@record.id)
52
- child_record_from_cache_hit = record_from_cache_hit.fetch_associated_records[0]
53
- assert_equal @associated_record, child_record_from_cache_hit
54
- assert_equal expected, child_record_from_cache_hit.fetch_deeply_associated_records
55
- end
56
-
57
- def test_on_cache_miss_child_record_fetch_should_include_nested_associations_to_avoid_n_plus_ones
58
- assert_queries(5) do
59
- # one for the top level record
60
- # one for the mid level has_many association
61
- # one for the mid level has_one association
62
- # one for the deep level level has_many on the mid level has_many association
63
- # one for the deep level level has_many on the mid level has_one association
64
- Item.fetch(@record.id)
65
- end
66
- end
67
-
68
- def test_saving_child_record_should_expire_parent_record
69
- IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
70
- if AssociatedRecord.primary_cache_index_enabled
71
- IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key)
72
- else
73
- IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key).never
74
- end
75
- @associated_record.name = 'different'
76
- @associated_record.save!
77
- end
78
-
79
- def test_saving_grand_child_record_should_expire_parent_record
80
- IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
81
- if AssociatedRecord.primary_cache_index_enabled
82
- IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key)
83
- else
84
- IdentityCache.cache.expects(:delete).with(@associated_record.primary_cache_index_key).never
85
- end
86
- IdentityCache.cache.expects(:delete).with(@deeply_associated_record.primary_cache_index_key)
87
- @deeply_associated_record.name = 'different'
88
- @deeply_associated_record.save!
89
- end
90
-
91
- def test_set_inverse_associations
92
- DeeplyAssociatedRecord.cache_belongs_to :associated_record
93
- AssociatedRecord.cache_belongs_to :item
94
- Item.fetch(@record.id) # warm cache
95
-
96
- item = Item.fetch(@record.id)
97
-
98
- assert_queries(0) do
99
- assert_memcache_operations(0) do
100
- associated_record = item.fetch_associated_records.to_a.first
101
- deeply_associated_record = associated_record.fetch_deeply_associated_records.first
102
- assert_equal item.id, deeply_associated_record.fetch_associated_record.fetch_item.id
103
- end
104
- end
105
- end
106
-
107
- end
108
-
109
- class RecursiveNormalizedHasManyTest < IdentityCache::TestCase
110
- def setup
111
- super
112
- AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
113
- Item.cache_has_many :associated_records, :embed => :ids
114
-
115
- @record = Item.new(:title => 'foo')
116
- @record.save
117
- @record.reload
118
- end
119
-
120
- def test_cache_repopulation_should_not_fetch_non_embedded_associations
121
- Item.any_instance.expects(:fetch_associated_records).never
122
- Item.fetch(@record.id) # cache miss
123
- end
124
- end
125
-
126
- class DisabledPrimaryIndexTest < RecursiveDenormalizedHasManyTest
127
- def setup
128
- super
129
- AssociatedRecord.disable_primary_cache_index
130
- end
131
- end
@@ -1,82 +0,0 @@
1
- require "test_helper"
2
-
3
- class SaveTest < IdentityCache::TestCase
4
- NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE
5
-
6
- def setup
7
- super
8
- Item.cache_index :title, :unique => true
9
- Item.cache_index :id, :title, :unique => true
10
-
11
- @record = Item.create(:title => 'bob')
12
- @blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:1"
13
- end
14
-
15
- def test_create
16
- @record = Item.new
17
- @record.title = 'bob'
18
-
19
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('2/bob')}")
20
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
21
- expect_cache_delete("#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:2").once
22
- @record.save
23
- end
24
-
25
- def test_update
26
- # Regular flow, write index id, write index id/tile, delete data blob since Record has changed
27
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/fred')}")
28
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('fred')}")
29
- expect_cache_delete(@blob_key)
30
-
31
- # Delete index id, delete index id/title
32
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/bob')}")
33
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
34
-
35
- @record.title = 'fred'
36
- @record.save
37
- end
38
-
39
- def test_destroy
40
- # Regular flow: delete data blob, delete index id, delete index id/tile
41
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/bob')}")
42
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
43
- expect_cache_delete(@blob_key)
44
-
45
- @record.destroy
46
- end
47
-
48
- def test_destroy_with_changed_attributes
49
- # Make sure to delete the old cache index key, since the new title never ended up in an index
50
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/bob')}")
51
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
52
- expect_cache_delete(@blob_key)
53
-
54
- @record.title = 'fred'
55
- @record.destroy
56
- end
57
-
58
- def test_touch_will_expire_the_caches
59
- # Regular flow: delete data blob, delete index id, delete index id/tile
60
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/bob')}")
61
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
62
- expect_cache_delete(@blob_key)
63
-
64
- @record.touch
65
- end
66
-
67
- def test_expire_cache_works_in_a_transaction
68
- expect_cache_delete("#{NAMESPACE}attr:Item:id:id/title:#{cache_hash('1/bob')}")
69
- expect_cache_delete("#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}")
70
- expect_cache_delete(@blob_key)
71
-
72
- ActiveRecord::Base.transaction do
73
- @record.send(:expire_cache)
74
- end
75
- end
76
-
77
- private
78
-
79
- def expect_cache_delete(key)
80
- @backend.expects(:write).with(key, IdentityCache::DELETED, anything)
81
- end
82
- end
@@ -1,112 +0,0 @@
1
- require "test_helper"
2
-
3
- class SchemaChangeTest < IdentityCache::TestCase
4
- class AddColumnToChild < ActiveRecord::Migration
5
- def up
6
- add_column :associated_records, :shiny, :string
7
- end
8
- end
9
-
10
- class AddColumnToDeepChild < ActiveRecord::Migration
11
- def up
12
- add_column :deeply_associated_records, :new_column, :string
13
- end
14
- end
15
-
16
- def setup
17
- super
18
- ActiveRecord::Migration.verbose = false
19
-
20
- read_new_schema
21
- Item.cache_has_one :associated, :embed => true
22
- AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
23
-
24
- @associated_record = AssociatedRecord.new(:name => 'bar')
25
- @deeply_associated_record = DeeplyAssociatedRecord.new(:name => "corge")
26
- @associated_record.deeply_associated_records << @deeply_associated_record
27
- @associated_record.deeply_associated_records << DeeplyAssociatedRecord.new(:name => "qux")
28
- @record = Item.new(:title => 'foo')
29
- @record.associated = @associated_record
30
-
31
- @associated_record.save!
32
- @record.save!
33
-
34
- @record.reload
35
- end
36
-
37
- def teardown
38
- active_records = [AssociatedRecord, DeeplyAssociatedRecord]
39
- super
40
- active_records.each {|ar| ar.reset_column_information }
41
- end
42
-
43
- # This helper simulates the models being reloaded
44
- def read_new_schema
45
- AssociatedRecord.reset_column_information
46
- DeeplyAssociatedRecord.reset_column_information
47
-
48
- AssociatedRecord.send(:instance_variable_set, :@rails_cache_key_prefix, nil)
49
- Item.send(:instance_variable_set, :@rails_cache_key_prefix, nil)
50
- end
51
-
52
- def test_schema_changes_on_embedded_association_should_cause_cache_miss_for_old_cached_objects
53
- record = Item.fetch(@record.id)
54
- record.fetch_associated
55
-
56
- AddColumnToChild.new.up
57
- read_new_schema
58
-
59
- Item.expects(:resolve_cache_miss).returns(@record)
60
- record = Item.fetch(@record.id)
61
- end
62
-
63
- def test_schema_changes_on_deeply_embedded_association_should_cause_cache_miss_for_old_cached_objects
64
- record = Item.fetch(@record.id)
65
- associated_record_from_cache = record.fetch_associated
66
- associated_record_from_cache.fetch_deeply_associated_records
67
-
68
- AddColumnToDeepChild.new.up
69
- read_new_schema
70
-
71
- Item.expects(:resolve_cache_miss).returns(@record)
72
- record = Item.fetch(@record.id)
73
- end
74
-
75
- def test_schema_changes_on_new_cached_child_association
76
- record = Item.fetch(@record.id)
77
-
78
- PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
79
- Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
80
- read_new_schema
81
-
82
- Item.expects(:resolve_cache_miss).returns(@record)
83
- record = Item.fetch(@record.id)
84
- end
85
-
86
- def test_embed_existing_cache_has_many
87
- PolymorphicRecord.include(IdentityCache)
88
- Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => :ids
89
- read_new_schema
90
-
91
- record = Item.fetch(@record.id)
92
-
93
- teardown_models
94
- setup_models
95
-
96
- PolymorphicRecord.include(IdentityCache::WithoutPrimaryIndex)
97
- Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
98
- read_new_schema
99
-
100
- record = Item.fetch(@record.id)
101
- end
102
-
103
- def test_add_non_embedded_cache_has_many
104
- PolymorphicRecord.include(IdentityCache)
105
- record = Item.fetch(@record.id)
106
-
107
- Item.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => :ids
108
- read_new_schema
109
-
110
- record = Item.fetch(@record.id)
111
- end
112
- end
@@ -1,16 +0,0 @@
1
- require "test_helper"
2
- require "helpers/serialization_format"
3
-
4
- class SerializationFormatChangeTest < IdentityCache::TestCase
5
- include SerializationFormat
6
-
7
- MESSAGE = "serialization format changed => increment IdentityCache.CACHE_VERSION and run rake update_serialization_format"
8
-
9
- def test_serialization_format_has_not_changed
10
- serialization = Marshal.load(serialize(serialized_record))
11
- preserialization = Marshal.load(File.binread(serialized_record_file))
12
- assert_equal(preserialization, serialization, MESSAGE)
13
- rescue SystemCallError
14
- assert(false, MESSAGE)
15
- end
16
- end