redrecord 0.1

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/CHANGELOG ADDED
@@ -0,0 +1 @@
1
+ v0.1. First version
data/Manifest ADDED
@@ -0,0 +1,7 @@
1
+ CHANGELOG
2
+ README
3
+ Rakefile
4
+ lib/redrecord.rb
5
+ test/test_all.rb
6
+ test/test_helper.rb
7
+ Manifest
data/README ADDED
@@ -0,0 +1,72 @@
1
+
2
+ redrecord
3
+ ---------
4
+
5
+ This gem pre-caches your ActiveRecord model's calculated attributes (computed
6
+ fields) in Redis.
7
+
8
+
9
+ Example
10
+ -------
11
+
12
+ class User < ActiveRecord::Base
13
+
14
+ cache do
15
+ def fullname
16
+ firstname + ' ' + lastname
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+
23
+ Methods defined inside the "cache" block are redefined to get the answer
24
+ from redis first. The cached attributes are saved whenever the record is
25
+ saved, in an after_commit callback.
26
+
27
+
28
+ Cache invalidation for associations
29
+ -----------------------------------
30
+
31
+ Redrecord can be used to cache attributes that use assocations:
32
+
33
+ class User < ActiveRecord::Base
34
+ has_many :preferences
35
+
36
+ cache do
37
+ def preferences_list
38
+ preferences.map(&:name).join(', ')
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ class Preference < ActiveRecord::Base
45
+ belongs_to :user
46
+
47
+ invalidate_cache_on :user
48
+
49
+ end
50
+
51
+
52
+ In this example, whenever a preference is saved, the associated user record
53
+ will be recalculated and saved in redis. If it is an array (eg. has_many)
54
+ then all of the associated records will be re-cached.
55
+
56
+ Other instance methods of interest:
57
+
58
+ obj.remove_from_cache! # Remove redis cache for an object.
59
+ obj.add_to_cache! # Recalculate fields and store in redis.
60
+ obj.cached_fields # hash of the cached fields and their values
61
+ obj.attribs_with_cached_fields # cached_fields merged with AR attributes
62
+
63
+
64
+
65
+
66
+
67
+ Contact the author
68
+ ------------------
69
+
70
+ Andrew Snow <andrew@modulus.org>
71
+ Andys^ on irc.freenode.net
72
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'echoe'
2
+
3
+ Echoe.new("redrecord") do |p|
4
+ p.author = "Andrew Snow"
5
+ p.email = 'andrew@modulus.org'
6
+ p.summary = "Redis cacheing for ActiveRecord"
7
+ p.url = "http://github.com/andys/redrecord"
8
+ p.runtime_dependencies = ['redis']
9
+ p.development_dependencies = ['active_model', 'active_support']
10
+ end
11
+
data/lib/redrecord.rb ADDED
@@ -0,0 +1,136 @@
1
+
2
+ class Redrecord
3
+
4
+ class << self
5
+ attr_accessor :redis
6
+ def update_queue
7
+ Thread.current[:redrecord_update_queue] ||= []
8
+ end
9
+ def is_marshalled?(str)
10
+ Marshal.dump(nil)[0,2] == str[0,2]
11
+ end
12
+ end
13
+
14
+ module Model
15
+
16
+ module ClassMethods
17
+
18
+ def redrecord_cached_fields
19
+ @redrecord_cached_fields ||= []
20
+ end
21
+
22
+ def redrecord_invalidation_fields
23
+ @redrecord_invalidation_fields ||= []
24
+ end
25
+
26
+ def cache(*fields, &bl)
27
+ if block_given?
28
+ old_methods = instance_methods
29
+ class_eval(&bl)
30
+ fields.push(*(instance_methods - old_methods))
31
+ end
32
+ redrecord_cached_fields.push(*fields)
33
+ fields.each do |f|
34
+ define_method "#{f}_with_cache" do
35
+ cached_method(f)
36
+ end
37
+ alias_method_chain f, :cache
38
+ end
39
+ end
40
+
41
+ def invalidate_cache_on(fieldname)
42
+ redrecord_invalidation_fields << fieldname.to_sym
43
+ end
44
+
45
+ end
46
+
47
+ def self.included(mod)
48
+ mod.extend(ClassMethods)
49
+ mod.send(:after_save, :redrecord_update_queue_save)
50
+ mod.send(:after_destroy, :redrecord_update_queue_destroy)
51
+ mod.send(:after_commit, :redrecord_update_queue_commit)
52
+ mod.send(:after_rollback, :redrecord_update_queue_rollback)
53
+ end
54
+
55
+ def redrecord_update_queue_save
56
+ Redrecord.update_queue << [:save, self] unless self.class.redrecord_cached_fields.empty?
57
+ invalidations_for_redrecord_update_queue
58
+ end
59
+
60
+ def invalidations_for_redrecord_update_queue
61
+ self.class.redrecord_invalidation_fields.each do |f|
62
+ if((field_value = send(f)).kind_of?(Array))
63
+ field_value.each {|item| Redrecord.update_queue << [:save, item] }
64
+ else
65
+ Redrecord.update_queue << [:save, field_value] if field_value
66
+ end
67
+ end
68
+ end
69
+
70
+ def redrecord_update_queue_destroy
71
+ Redrecord.update_queue << [:destroy, self] unless self.class.redrecord_cached_fields.empty?
72
+ invalidations_for_redrecord_update_queue
73
+ end
74
+
75
+ def redrecord_update_queue_rollback
76
+ Redrecord.update_queue.clear
77
+ end
78
+
79
+ def redrecord_update_queue_commit
80
+ Redrecord.update_queue.each do |command, record|
81
+ if command == :destroy
82
+ record.remove_from_cache!
83
+ elsif command == :save
84
+ record.add_to_cache!
85
+ end
86
+ end
87
+ Redrecord.update_queue.clear
88
+ end
89
+
90
+ def redrecord_key
91
+ "#{self.class.table_name}:#{self.id}"
92
+ end
93
+
94
+ def remove_from_cache!
95
+ Redrecord.redis.del redrecord_key
96
+ end
97
+
98
+ def add_to_cache!
99
+ Redrecord.redis.hmset(redrecord_key,
100
+ *(self.class.redrecord_cached_fields.map {|f|
101
+ val = send("#{f}_without_cache")
102
+ [f.to_s, String===val && !Redrecord.is_marshalled?(val) ? val : Marshal.dump(val)]
103
+ }.flatten)
104
+ )
105
+ end
106
+
107
+ def cached_method(method_name)
108
+ redrecord_cached_attrib_hash[method_name.to_sym]
109
+ end
110
+
111
+ def redrecord_redis_cache
112
+ @redrecord_redis_cache ||= Redrecord.redis.hgetall(redrecord_key)
113
+ end
114
+
115
+ def redrecord_cached_attrib_hash
116
+ @redrecord_cached_attrib_hash ||= Hash.new do |h,k|
117
+ h[k.to_sym] = if(cached = (redrecord_redis_cache[k.to_s] unless new_record?))
118
+ Redrecord.is_marshalled?(cached) ? Marshal.load(cached) : cached
119
+ else
120
+ send("#{k}_without_cache")
121
+ end
122
+ end
123
+ end
124
+
125
+ def attribs_with_cached_fields
126
+ attributes.merge(cached_fields)
127
+ end
128
+
129
+ def cached_fields
130
+ self.class.redrecord_cached_fields.inject({}) {|hsh,field| hsh[field] = send(field) ; hsh }
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+
data/redrecord.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "redrecord"
5
+ s.version = "0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andrew Snow"]
9
+ s.date = "2012-01-23"
10
+ s.description = "Redis cacheing for ActiveRecord"
11
+ s.email = "andrew@modulus.org"
12
+ s.extra_rdoc_files = ["CHANGELOG", "README", "lib/redrecord.rb"]
13
+ s.files = ["CHANGELOG", "README", "Rakefile", "lib/redrecord.rb", "test/test_all.rb", "test/test_helper.rb", "Manifest", "redrecord.gemspec"]
14
+ s.homepage = "http://github.com/andys/redrecord"
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Redrecord", "--main", "README"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = "redrecord"
18
+ s.rubygems_version = "1.8.10"
19
+ s.summary = "Redis cacheing for ActiveRecord"
20
+ s.test_files = ["test/test_all.rb"]
21
+
22
+ if s.respond_to? :specification_version then
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<redis>, [">= 0"])
27
+ s.add_development_dependency(%q<active_model>, [">= 0"])
28
+ s.add_development_dependency(%q<active_support>, [">= 0"])
29
+ else
30
+ s.add_dependency(%q<redis>, [">= 0"])
31
+ s.add_dependency(%q<active_model>, [">= 0"])
32
+ s.add_dependency(%q<active_support>, [">= 0"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<redis>, [">= 0"])
36
+ s.add_dependency(%q<active_model>, [">= 0"])
37
+ s.add_dependency(%q<active_support>, [">= 0"])
38
+ end
39
+ end
data/test/test_all.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'active_model'
2
+ require 'active_support/core_ext/module/aliasing'
3
+ require "#{File.dirname(__FILE__)}/../lib/redrecord"
4
+ require 'test/unit'
5
+ require 'ostruct'
6
+ require 'redis'
7
+ require "#{File.dirname(__FILE__)}/test_helper"
8
+
9
+ class TestRedrecord < Test::Unit::TestCase
10
+ def setup
11
+ $redis.flushdb
12
+ @user = TestUser.new(1, 'John', 'Smith')
13
+ $saved = {}
14
+ end
15
+
16
+ def test_cached_string_attribute_save
17
+ # start with a blank slate and save the record
18
+ assert_equal({}, $redis.hgetall('TestUser:1'))
19
+ assert_nil @user.recalculated
20
+ @user.save
21
+
22
+ # It should now be saved in redis
23
+ assert @user.recalculated
24
+ assert_equal 'John Smith', $redis.hget('TestUser:1', 'full_name')
25
+
26
+ # different object should get the value straight of redis
27
+ u2 = TestUser.new(1)
28
+ assert_equal 'John Smith', u2.full_name
29
+ assert_nil u2.recalculated
30
+ end
31
+
32
+
33
+ def test_cached_nil_attribute_save
34
+ @user.save
35
+ assert_equal Marshal.dump(nil), $redis.hget('TestUser:1', 'nil')
36
+ u2 = TestUser.new(1, 'John', 'Smith')
37
+ assert_nil u2.nil
38
+ end
39
+
40
+ def test_invalidation_on_save
41
+ @user.save
42
+ @user.first_name = 'Bob'
43
+ @user.save
44
+
45
+ assert_equal 'Bob Smith', $redis.hget('TestUser:1', 'full_name')
46
+
47
+ u2 = TestUser.new(1)
48
+ assert_equal 'Bob Smith', u2.full_name
49
+ assert_nil u2.recalculated
50
+ end
51
+
52
+ def test_invalidation_on_delete
53
+ @user.save
54
+ assert_equal 'John Smith', $redis.hget('TestUser:1', 'full_name')
55
+ @user.destroy
56
+ assert_equal({}, $redis.hgetall('TestUser:1'))
57
+ end
58
+
59
+ def test_invalidation_on_rollback
60
+ @user.save_with_rollback
61
+ assert_equal({}, $redis.hgetall('TestUser:1'))
62
+ end
63
+
64
+ def test_invalidation_on_association_create
65
+ # Create two groups which are attached to the user
66
+ TestGroup.new(1, 'users', @user).save
67
+ TestGroup.new(2, 'admins', @user).save
68
+ #@user.save
69
+
70
+ # Now a fresh user record should get the answer out of cache
71
+ u2 = TestUser.new(1)
72
+ assert_equal ['admins', 'users'], u2.group_names
73
+ assert_nil u2.recalculated
74
+ end
75
+
76
+ def test_invalidation_on_association
77
+ # Create two groups which are attached to the user
78
+ TestGroup.new(1, 'users', @user).save
79
+ g = TestGroup.new(2, 'admins', @user).save
80
+ @user.save
81
+
82
+ # Removing a group should also refresh the cache
83
+ g.destroy
84
+ assert_equal ['users'], TestUser.new(1).group_names
85
+ end
86
+
87
+ def test_attribs
88
+ assert_equal({
89
+ :nil => nil,
90
+ :group_names => [],
91
+ :full_name => 'John Smith'},
92
+ @user.cached_fields)
93
+ end
94
+
95
+ def test_cached_fields
96
+ assert_equal({
97
+ :first_name => 'John',
98
+ :last_name => 'Smith',
99
+ :id => 1,
100
+ :nil => nil,
101
+ :group_names => [],
102
+ :full_name => 'John Smith'},
103
+ @user.attribs_with_cached_fields)
104
+ end
105
+
106
+
107
+ end
@@ -0,0 +1,68 @@
1
+ $redis = Redis.new(:host => 'localhost', :port => 6379) # TODO: dangerous!
2
+ Redrecord.redis = $redis
3
+
4
+ class TestModel
5
+ extend ActiveModel::Callbacks
6
+
7
+ define_model_callbacks :save, :destroy, :commit, :rollback
8
+
9
+ include Redrecord::Model
10
+
11
+ def save
12
+ run_callbacks :commit do
13
+ run_callbacks :save do
14
+ $saved[redrecord_key] = self
15
+ end
16
+ end
17
+ end
18
+ def destroy
19
+ run_callbacks :commit do
20
+ run_callbacks :destroy do
21
+ $saved.delete(redrecord_key)
22
+ end
23
+ end
24
+ end
25
+ def save_with_rollback
26
+ run_callbacks :rollback do
27
+ run_callbacks :save do
28
+ end
29
+ end
30
+ end
31
+ def new_record?
32
+ !$saved[redrecord_key]
33
+ end
34
+ def self.table_name
35
+ self.to_s.split('::').last
36
+ end
37
+ def attributes
38
+ instance_variables.map {|var| var.to_s.gsub(/^@/,'').to_sym }.inject({}) {|hsh,var| hsh[var] = send(var) ; hsh}
39
+ end
40
+ end
41
+
42
+ class TestGroup < TestModel
43
+ attr_accessor :id, :name, :user
44
+ def initialize(id, name=nil, user=nil)
45
+ @id, @name, @user = id, name, user
46
+ end
47
+ invalidate_cache_on :user
48
+ end
49
+
50
+ class TestUser < TestModel
51
+ attr_accessor :first_name, :last_name, :id, :recalculated
52
+ def initialize(id, first_name=nil, last_name=nil)
53
+ @id, @first_name, @last_name = id, first_name, last_name
54
+ end
55
+
56
+ cache do
57
+ def full_name
58
+ @recalculated = true
59
+ "#{first_name} #{last_name}"
60
+ end
61
+ def nil
62
+ nil
63
+ end
64
+ def group_names
65
+ $saved.values.select {|v| TestGroup===v && v.user.id == self.id }.map(&:name).sort
66
+ end
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redrecord
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Snow
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &71893080 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *71893080
25
+ - !ruby/object:Gem::Dependency
26
+ name: active_model
27
+ requirement: &71892570 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *71892570
36
+ - !ruby/object:Gem::Dependency
37
+ name: active_support
38
+ requirement: &71891930 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *71891930
47
+ description: Redis cacheing for ActiveRecord
48
+ email: andrew@modulus.org
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files:
52
+ - CHANGELOG
53
+ - README
54
+ - lib/redrecord.rb
55
+ files:
56
+ - CHANGELOG
57
+ - README
58
+ - Rakefile
59
+ - lib/redrecord.rb
60
+ - test/test_all.rb
61
+ - test/test_helper.rb
62
+ - Manifest
63
+ - redrecord.gemspec
64
+ homepage: http://github.com/andys/redrecord
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --line-numbers
69
+ - --inline-source
70
+ - --title
71
+ - Redrecord
72
+ - --main
73
+ - README
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '1.2'
88
+ requirements: []
89
+ rubyforge_project: redrecord
90
+ rubygems_version: 1.8.10
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Redis cacheing for ActiveRecord
94
+ test_files:
95
+ - test/test_all.rb