redis-native_hash 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,30 +1,24 @@
1
- require 'redis'
2
- require 'core_ext/hash' unless defined?(ActiveSupport)
3
- require 'redis/marshal'
4
- require 'redis/tracked_hash'
5
-
6
- if defined?(Rack::Session)
7
- require "rack/session/abstract/id"
8
- require 'rack/session/redis_hash'
9
- end
1
+ require_relative "../redis_hash"
10
2
 
11
3
  require 'securerandom'
12
4
 
13
5
  class Redis
14
6
  class NativeHash < TrackedHash
7
+ include ClientHelper
8
+ include KeyHelper
15
9
 
16
10
  attr_accessor :namespace
17
11
 
18
- def initialize(*args)
12
+ def initialize(aargh = nil)
19
13
  super(nil)
20
- track!
21
- if args.first.kind_of?(String) or args.first.kind_of?(Symbol)
22
- self.namespace = args.shift
23
- elsif !self.instance_of?(NativeHash) # use class name as default namespace for user defined classes
24
- self.namespace = self.class.to_s.downcase
14
+ case aargh
15
+ when String,Symbol
16
+ self.namespace = aargh
17
+ when Hash
18
+ self.namespace = aargh.keys.first
19
+ self.key = aargh.values.first
25
20
  end
26
- data = args.shift
27
- update(data) if data.kind_of?(Hash)
21
+ track!
28
22
  end
29
23
 
30
24
  def []=(key, value)
@@ -55,44 +49,44 @@ class Redis
55
49
  indices.collect { |key| self[ convert_key(key) ] }
56
50
  end
57
51
 
58
- def key
59
- @key ||= self.class.generate_key
60
- end
61
-
62
52
  def key=(new_key)
63
53
  renew_key(new_key)
64
54
  end
65
55
 
66
56
  attr_writer :version
67
57
  def version
68
- @version ||= self.class.generate_key
69
- end
70
-
71
- def save( attempt = 0 )
72
- fail "Unable to save Redis::Hash after max attempts." if attempt > 5
73
- redis.watch redis_key
74
- latest_version = redis.hget(redis_key, "__version")
75
- reload! unless ( latest_version.nil? || latest_version == self.version )
76
- self.version = nil # generate new version token
77
- changed_keys = (self.changed + self.added).uniq
78
- changes = []
79
- changed_keys.each do |key|
80
- changes.push( key, Redis::Marshal.dump(self[key]) )
81
- end
82
- deleted_keys = self.deleted
83
- unless deleted_keys.empty? and changes.empty?
58
+ @version ||= generate_key
59
+ end
60
+
61
+ def save( max_attempts = 5 )
62
+ (1..max_attempts).each do |n|
63
+ redis.watch redis_key
64
+ latest_version = redis.hget(redis_key, "__version")
65
+ reload! unless ( latest_version.nil? || latest_version == self.version )
66
+ self.version = nil # generate new version token
67
+ changed_keys = (self.changed + self.added).uniq
68
+ changes = []
69
+ changed_keys.each do |key|
70
+ changes.push( key, Redis::Marshal.dump(self[key]) )
71
+ end
72
+ deleted_keys = self.deleted
73
+ if deleted_keys.empty? and changes.empty?
74
+ redis.unwatch
75
+ return true
76
+ end
84
77
  success = redis.multi do
85
78
  redis.hmset( redis_key, *changes.push("__version", self.version) ) unless changes.empty?
86
79
  deleted_keys.each { |key| redis.hdel( redis_key, key) }
87
80
  end
88
81
  if success
89
- untrack!; track! #reset!
90
- else
91
- save( attempt + 1 )
82
+ untrack!; track! #reset hash
83
+ return true
92
84
  end
93
- else
94
- redis.unwatch
95
85
  end
86
+ raise "Unable to save hash after max attempts (#{max_attempts}). " +
87
+ "Amazing concurrency event may be underway. " +
88
+ "Make some popcorn."
89
+ false
96
90
  end
97
91
 
98
92
  def update(data)
@@ -106,8 +100,14 @@ class Redis
106
100
  super(data.stringify_keys!)
107
101
  end
108
102
 
103
+ def replace(other_hash)
104
+ clear
105
+ update(other_hash)
106
+ end
107
+
109
108
  def reload!
110
- self.update( self.class.find( {namespace=>key} ) )
109
+ hash = self.class.find( namespace ? {namespace => key} : key )
110
+ self.update( hash ) if hash
111
111
  end
112
112
  alias_method :reload, :reload!
113
113
 
@@ -127,79 +127,66 @@ class Redis
127
127
  key
128
128
  end
129
129
 
130
- def self.redis
131
- @@redis ||= Redis.new
132
- end
133
-
134
- def self.redis=(resource)
135
- @@redis = resource
136
- end
137
-
138
- def self.generate_key
139
- t = Time.now
140
- t.strftime('%Y%m%d%H%M%S.') + t.usec.to_s.rjust(6,'0') + '.' + SecureRandom.hex(16)
130
+ def expire(seconds)
131
+ redis.expire(redis_key, seconds)
141
132
  end
142
133
 
143
- def self.find(params)
144
- case params
145
- when Hash
146
- hashes = []
147
- params.each_pair do |namespace, key|
148
- result = fetch_values( "#{namespace}:#{key}" )
149
- unless result.empty?
150
- hashes << build(namespace,key,result)
134
+ class << self
135
+ def find(params)
136
+ case params
137
+ when Hash
138
+ hashes = []
139
+ params.each_pair do |namespace, key|
140
+ result = fetch_values( "#{namespace}:#{key}" )
141
+ unless result.empty?
142
+ hashes << build(namespace,key,result)
143
+ end
151
144
  end
145
+ unless hashes.empty?
146
+ hashes.size == 1 ? hashes.first : hashes
147
+ else
148
+ nil
149
+ end
150
+ when String,Symbol
151
+ unless self == Redis::NativeHash
152
+ namespace = self.new.namespace.to_s
153
+ namespace = "#{namespace}:" unless namespace.empty?
154
+ result = fetch_values( "#{namespace}#{params}" )
155
+ else
156
+ result = fetch_values(params)
157
+ end
158
+ result.empty? ? nil : build(nil,params,result)
152
159
  end
153
- unless hashes.empty?
154
- hashes.size == 1 ? hashes.first : hashes
155
- else
156
- nil
157
- end
158
- when String
159
- unless self.instance_of?(NativeHash)
160
- result = fetch_values( "#{self.new.namespace}:#{params}" )
161
- else
162
- result = fetch_values(params)
163
- end
164
- result.empty? ? nil : build(nil,params,result)
165
160
  end
166
- end
167
-
168
- def self.build(namespace, key, values)
169
- h = self.new
170
- h.namespace = namespace
171
- h.key = key
172
- h.populate(values)
173
- h
174
- end
175
-
176
- def self.fetch_values(key)
177
- results = redis.hgetall(key)
178
- results.each_pair { |key,value| results[key] = Redis::Marshal.load(value) }
179
- end
180
161
 
181
- def self.attr_persist(*attributes)
182
- attributes.each do |attr|
183
- class_eval <<-EOS
184
- def #{attr}=(value)
185
- self["#{attr}"] = value
186
- end
187
-
188
- def #{attr}
189
- self["#{attr}"]
190
- end
191
- EOS
162
+ def build(namespace, key, values)
163
+ h = self.new
164
+ h.namespace = namespace
165
+ h.key = key
166
+ h.populate(values)
167
+ h
192
168
  end
193
- end
194
169
 
195
- protected
196
- def redis; self.class.redis; end
197
- def redis_key
198
- namespace.nil? ? key : "#{namespace}:#{key}"
170
+ def fetch_values(key)
171
+ results = redis.hgetall(key)
172
+ results.each_pair { |key,value| results[key] = Redis::Marshal.load(value) }
199
173
  end
200
- def convert_key(key)
201
- key.to_s
174
+
175
+ def attr_persist(*attributes)
176
+ attributes.each do |attr|
177
+ class_eval <<-EOS
178
+ def #{attr}=(value)
179
+ self["#{attr}"] = value
180
+ end
181
+
182
+ def #{attr}
183
+ self["#{attr}"]
184
+ end
185
+ EOS
186
+ end
202
187
  end
188
+
189
+ end
203
190
  end
204
191
  end
205
192
 
@@ -1,42 +1,52 @@
1
1
  class Redis
2
2
  class TrackedHash < Hash
3
-
3
+
4
4
  def original
5
5
  @original ||= self.dup
6
6
  end
7
7
  alias_method :track, :original
8
8
  alias_method :track!, :original
9
-
9
+
10
10
  def untrack
11
11
  @original = nil
12
12
  end
13
13
  alias_method :untrack!, :untrack
14
-
14
+
15
15
  def retrack
16
16
  untrack!
17
17
  track!
18
18
  end
19
19
  alias_method :retrack!, :retrack
20
-
20
+
21
21
  def changed
22
22
  changes = keys.select do |key|
23
23
  self[key] != original[key]
24
24
  end
25
25
  end
26
-
26
+
27
27
  def deleted
28
28
  original.keys - self.keys
29
29
  end
30
-
30
+
31
31
  def added
32
32
  self.keys - original.keys
33
33
  end
34
-
34
+
35
35
  def populate(other_hash)
36
36
  update(other_hash)
37
37
  retrack!
38
38
  end
39
-
39
+
40
+ def dup
41
+ dupe = super
42
+ # duplicate a little deeper
43
+ # otherwise, object references will make it appear a value hasn't changed when it has
44
+ self.keys.each do |k|
45
+ dupe[k] = self[k].dup rescue self[k]
46
+ end
47
+ dupe
48
+ end
49
+
40
50
  def update(other_hash)
41
51
  if other_hash.kind_of?(TrackedHash)
42
52
  other_original = other_hash.original
@@ -51,6 +61,7 @@ class Redis
51
61
  end
52
62
  end
53
63
  alias_method :merge!, :update
54
-
64
+
55
65
  end
56
66
  end
67
+
data/lib/redis_hash.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'redis'
2
+ require 'core_ext/hash' unless defined?(ActiveSupport)
3
+ require 'redis/marshal'
4
+ require 'redis/tracked_hash'
5
+ require 'redis/client_helper'
6
+ require 'redis/key_helper'
7
+ require 'redis/big_hash'
8
+ require 'redis/native_hash'
9
+ require 'redis/lazy_hash'
10
+
11
+ if defined?(Rack::Session)
12
+ require "rack/session/abstract/id"
13
+ require 'rack/session/redis_hash'
14
+ end
15
+
16
+ if defined?(ActionDispatch::Session)
17
+ require 'action_dispatch/session/redis_hash'
18
+ end
19
+
20
+ if defined?(ActiveSupport)
21
+ require "active_support/cache/redis_store"
22
+ end
@@ -4,14 +4,14 @@
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = %q{redis-native_hash}
8
- s.version = "0.1.0"
7
+ s.name = "redis-native_hash"
8
+ s.version = "0.2.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Carl Zulauf", "Adam Lassek"]
12
- s.date = %q{2011-07-13}
13
- s.description = %q{ruby-hash-to-redis-hash mapping}
14
- s.email = %q{czulauf@lyconic.com}
12
+ s.date = "2012-05-13"
13
+ s.description = "ruby-hash-to-redis-hash mapping"
14
+ s.email = "czulauf@lyconic.com"
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
17
17
  "README.mkd"
@@ -24,24 +24,35 @@ Gem::Specification.new do |s|
24
24
  "LICENSE.txt",
25
25
  "README.mkd",
26
26
  "Rakefile",
27
+ "TODO.mkd",
27
28
  "VERSION",
29
+ "lib/action_dispatch/session/redis_hash.rb",
30
+ "lib/active_support/cache/redis_hash.rb",
31
+ "lib/active_support/cache/redis_store.rb",
28
32
  "lib/core_ext/hash.rb",
29
33
  "lib/rack/session/redis_hash.rb",
34
+ "lib/redis/big_hash.rb",
35
+ "lib/redis/client_helper.rb",
36
+ "lib/redis/key_helper.rb",
37
+ "lib/redis/lazy_hash.rb",
30
38
  "lib/redis/marshal.rb",
31
39
  "lib/redis/native_hash.rb",
32
40
  "lib/redis/tracked_hash.rb",
41
+ "lib/redis_hash.rb",
33
42
  "redis-native_hash.gemspec",
34
43
  "spec/redis-hash_spec.rb",
35
- "spec/redis/redis_hash_spec.rb",
44
+ "spec/redis/big_hash_spec.rb",
45
+ "spec/redis/lazy_hash_spec.rb",
46
+ "spec/redis/native_hash_spec.rb",
36
47
  "spec/spec_helper.rb",
37
48
  "spec/tracked_hash_spec.rb",
38
49
  "spec/user_defined/user_spec.rb"
39
50
  ]
40
- s.homepage = %q{http://github.com/carlzulauf/redis-native_hash}
51
+ s.homepage = "http://github.com/carlzulauf/redis-native_hash"
41
52
  s.licenses = ["MIT"]
42
53
  s.require_paths = ["lib"]
43
- s.rubygems_version = %q{1.6.2}
44
- s.summary = %q{ruby-hash-to-redis-hash mapping}
54
+ s.rubygems_version = "1.8.15"
55
+ s.summary = "ruby-hash-to-redis-hash mapping"
45
56
 
46
57
  if s.respond_to? :specification_version then
47
58
  s.specification_version = 3
@@ -0,0 +1,118 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Redis::BigHash do
4
+ before :each do
5
+ @hash = Redis::BigHash.new
6
+ @hash[:foo] = "bar"
7
+ @hash[:yin] = "yang"
8
+ end
9
+
10
+ describe "#[]" do
11
+ it "should read an existing value" do
12
+ @hash[:foo].should == "bar"
13
+ end
14
+ it "should get nil for a value that doesn't exist" do
15
+ @hash[:bad_key].should be_nil
16
+ end
17
+ it "should allow lookup of multiple keys, returning an array" do
18
+ @hash[:foo, :yin, :bad_key].should == ["bar", "yang", nil]
19
+ end
20
+ end
21
+
22
+ describe "#[]=" do
23
+ it "should store string values" do
24
+ str = "This is a test string"
25
+ @hash[:test] = str
26
+ @hash[:test].should == str
27
+ end
28
+ it "should store arbitrary objects" do
29
+ t = Time.now
30
+ @hash[:test] = t
31
+ @hash[:test].should == t
32
+ end
33
+ end
34
+
35
+ describe "#add" do
36
+ it "should not overwrite an existing value" do
37
+ @hash.add(:foo, "bad value")
38
+ @hash[:foo].should == "bar"
39
+ end
40
+ it "should set a value when it doesn't exist" do
41
+ @hash.add(:new_key, "good value")
42
+ @hash[:new_key].should == "good value"
43
+ end
44
+ end
45
+
46
+ describe "#key=" do
47
+ it "should change the key used" do
48
+ @hash[:testing] = "key change"
49
+ @hash.key = "some_new_key"
50
+ @hash.key.should == "some_new_key"
51
+ end
52
+ it "should move all hash data to a new key" do
53
+ @hash[:testing] = "hash migration"
54
+ @hash.key = "some_new_key"
55
+ @hash[:foo].should == "bar"
56
+ end
57
+ it "should not whine when the hash is empty" do
58
+ hash = Redis::BigHash.new :frequent => :plyer
59
+ hash.key = :flyer
60
+ hash.key.should == :flyer
61
+ end
62
+ end
63
+
64
+ describe "#keys" do
65
+ it "should return a list of all keys" do
66
+ @hash.keys.should == ["foo", "yin"]
67
+ end
68
+ end
69
+
70
+ describe "#key?" do
71
+ it "should return true when a key is present" do
72
+ @hash.key?(:foo).should be_true
73
+ end
74
+ it "should return false when a key is not present" do
75
+ @hash.key?(:fubar).should be_false
76
+ end
77
+ end
78
+
79
+ describe "#update" do
80
+ it "should update BigHash with values from another hash" do
81
+ @hash.update :test1 => "value1", :test2 => "value2"
82
+ @hash[:test1].should == "value1"
83
+ @hash[:test2].should == "value2"
84
+ end
85
+ it "should allow values to be arbitrary ruby objects" do
86
+ t = Time.now; r = Rational(22,7)
87
+ @hash.update :test1 => t, :test2 => r
88
+ @hash[:test1].should == t
89
+ @hash[:test2].to_s.should == "22/7"
90
+ end
91
+ end
92
+
93
+ describe "#delete" do
94
+ it "should return the current value" do
95
+ @hash.delete(:foo).should == "bar"
96
+ end
97
+ it "should remove the value" do
98
+ @hash.delete(:foo)
99
+ @hash[:foo].should be_nil
100
+ end
101
+ end
102
+
103
+ describe "#namespace" do
104
+ it "should prepend the namespace onto the key" do
105
+ @hash.namespace = "test_namespace"
106
+ @hash.redis_key.should =~ /^test_namespace:/
107
+ end
108
+ it "should migrate existing values over" do
109
+ @hash.namespace = "test_namespace"
110
+ @hash[:foo].should == "bar"
111
+ end
112
+ end
113
+
114
+ after :each do
115
+ @hash.destroy
116
+ end
117
+ end
118
+
@@ -0,0 +1,160 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ # MOST OF THIS COPIED FROM native_hash_spec !!!!
4
+ # Redis::LazyHash should behave pretty much the same as Redis::LazyHash
5
+ # only, lazier...
6
+
7
+ describe Redis::LazyHash do
8
+ before :each do
9
+ @hash = Redis::LazyHash.new :test
10
+ @hash.update("foo" => "bar")
11
+ @hash.save
12
+ @hash = Redis::LazyHash.new :test => @hash.key
13
+ end
14
+
15
+ describe "#loaded?" do
16
+ it "should not be loaded when no read/write has occurred" do
17
+ @hash.loaded?.should be_false
18
+ end
19
+ it "should be loaded after a read occurs" do
20
+ @hash[:foo]
21
+ @hash.loaded?.should be_true
22
+ end
23
+ end
24
+
25
+ describe "#save" do
26
+ it "should presist changes to existing hash key" do
27
+ @hash["foo"] = "something else"
28
+ @hash.save
29
+ hash = Redis::LazyHash.find :test => @hash.key
30
+ hash["foo"].should == "something else"
31
+ end
32
+ it "should persist new hash keys" do
33
+ @hash["yin"] = "yang"
34
+ @hash.save
35
+ hash = Redis::LazyHash.find :test => @hash.key
36
+ hash["yin"].should == "yang"
37
+ end
38
+ it "should remove deleted keys from redis" do
39
+ @hash["yin"] = "yang"
40
+ @hash.delete("foo")
41
+ @hash["foo"].should == nil
42
+ @hash.save
43
+ hash = Redis::LazyHash.find :test => @hash.key
44
+ hash["foo"].should == nil
45
+ end
46
+ it "should respect changes made since last read from redis" do
47
+ @hash.inspect # have to touch the hash first
48
+ concurrent_edit = Redis::LazyHash.find :test => @hash.key
49
+ concurrent_edit["foo"] = "race value"
50
+ concurrent_edit.save
51
+ @hash["yin"] = "yang"
52
+ @hash["foo"] = "bad value"
53
+ @hash.save
54
+ hash = Redis::LazyHash.find :test => @hash.key
55
+ hash["foo"].should == "race value"
56
+ hash["yin"].should == "yang"
57
+ end
58
+ it "should respect removed hash keys since last read" do
59
+ @hash.inspect
60
+ concurrent_edit = Redis::LazyHash.find :test => @hash.key
61
+ concurrent_edit["yin"] = "yang"
62
+ concurrent_edit.delete("foo")
63
+ concurrent_edit.save
64
+ @hash["foo"] = "bad value"
65
+ @hash.save
66
+ hash = Redis::LazyHash.find :test => @hash.key
67
+ hash["foo"].should == nil
68
+ hash["yin"].should == "yang"
69
+ end
70
+ it "should allow overwrite of concurrent edit after #reload! is called" do
71
+ @hash.inspect
72
+ concurrent_edit = Redis::LazyHash.find :test => @hash.key
73
+ concurrent_edit["yin"] = "yang"
74
+ concurrent_edit.delete("foo")
75
+ concurrent_edit.save
76
+ @hash.reload!
77
+ @hash["foo"].should == nil
78
+ @hash["foo"] = "good value"
79
+ @hash.save
80
+ hash = Redis::LazyHash.find :test => @hash.key
81
+ hash["foo"].should == "good value"
82
+ end
83
+ it "should treat string and symbolic keys the same" do
84
+ @hash[:foo].should == "bar"
85
+ @hash[:test] = "good value"
86
+ @hash["test"].should == "good value"
87
+ @hash.save
88
+ hash = Redis::LazyHash.find :test => @hash.key
89
+ hash[:test].should == "good value"
90
+ hash["test"].should == "good value"
91
+ end
92
+ it "should properly store nested hashes" do
93
+ @hash[:test] = { :foo => :bar, :x => { :y => "z" } }
94
+ @hash[:test][:x][:y].should == "z"
95
+ @hash.save
96
+ hash = Redis::LazyHash.find :test => @hash.key
97
+ hash[:test][:foo].should == :bar
98
+ hash[:test][:x][:y].should == "z"
99
+ end
100
+ end
101
+
102
+ describe "#renew" do
103
+ it "should generate a new key" do
104
+ old_key = @hash.key
105
+ new_key = @hash.renew_key
106
+ @hash.key.should eq(new_key)
107
+ @hash.key.should_not eq(old_key)
108
+ end
109
+ it "should remove the old hash from redis" do
110
+ old_key = @hash.key
111
+ namespace = @hash.namespace
112
+ @hash.renew_key
113
+ hash = Redis::LazyHash.find namespace => old_key
114
+ hash.inspect
115
+ hash.size.should == 0
116
+ end
117
+ it "should not persist the hash under the new key until #save is called" do
118
+ @hash["good key"] = "good value"
119
+ key = @hash.renew_key
120
+ bad_hash = Redis::LazyHash.find :test => key
121
+ bad_hash.size.should == 0
122
+ @hash.save
123
+ good_hash = Redis::LazyHash.find :test => key
124
+ good_hash["good key"].should eq("good value")
125
+ good_hash["foo"].should eq("bar")
126
+ end
127
+ end
128
+
129
+ describe "#key=" do
130
+ it "should allow an arbitrary key to be used" do
131
+ @hash.key = "blah@blah.com"
132
+ @hash.save
133
+ a_hash = Redis::LazyHash.find :test => "blah@blah.com"
134
+ a_hash["foo"].should eq('bar')
135
+ end
136
+ it "should not leave the old hash behind when the key is changed" do
137
+ old_key = @hash.key
138
+ @hash.key = "carl@linkleaf.com"
139
+ @hash.save
140
+ bad_hash = Redis::LazyHash.find :test => old_key
141
+ bad_hash.size.should == 0
142
+ end
143
+ end
144
+
145
+ describe ".find" do
146
+ it "should find an existing redis hash" do
147
+ hash = Redis::LazyHash.find :test => @hash.key
148
+ hash["foo"].should == "bar"
149
+ end
150
+ it "should return an empty hash when hash not found" do
151
+ hash = Redis::LazyHash.find :foo => :doesnt_exist
152
+ hash.size.should == 0
153
+ end
154
+ end
155
+
156
+
157
+ after :each do
158
+ @hash.destroy
159
+ end
160
+ end
@@ -2,7 +2,8 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe Redis::NativeHash do
4
4
  before :each do
5
- @hash = Redis::NativeHash.new :test, "foo" => "bar"
5
+ @hash = Redis::NativeHash.new :test
6
+ @hash.update("foo" => "bar")
6
7
  @hash.save
7
8
  end
8
9