redrecord 0.1

Sign up to get free protection for your applications and to get access to all the features.
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