redis-native_hash 0.1.0 → 0.2.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.
@@ -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