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.
data/README.mkd CHANGED
@@ -1,8 +1,146 @@
1
- = redis-native_hash
1
+ # redis-native_hash
2
2
 
3
- Description goes here.
3
+ Tools to help expose Redis' powerful Hash type through a familiar Ruby `Hash` interface.
4
+ `NativeHash` provides a general solution for exposing reasonably sized Redis hashes as
5
+ Ruby hashes, including sane and transparent transactions, nested hash support, and automatic
6
+ serialization of complex data types. `BigHash` is provided to
7
+ efficiently handle big or even enormous Redis hashes. `LazyHash` is a convenient proxy
8
+ for `NativeHash` useful when you aren't sure the hash will be read (useful for sessions).
4
9
 
5
- == Contributing to redis-native_hash
10
+ Also included is Rack middleware to store sessions in Redis hashes, and a two Rails caching
11
+ solution, one using Redis hashes and the other using Redis strings.
12
+
13
+ ## Example usage for `NativeHash`
14
+
15
+ ```ruby
16
+ require "redis_hash"
17
+
18
+ # Create a Ruby hash backed by Redis
19
+ hash = Redis::NatveHash.new # => {}
20
+ hash[:foo] = :bar
21
+ hash.key # => "20120512181125.368617.04d2abae82db62ece82b3805b654082b"
22
+ hash.save # => true
23
+
24
+ # Retrieve an existing hash from Redis
25
+ existing = Redis::NativeHash.find(hash.key) # => {"foo" => :bar}
26
+
27
+ # Symbols and strings can be used interchangeably, sort of like HashWithIndifferentAccess
28
+ existing[:foo] # => :bar
29
+ existing["foo"] # => :bar
30
+
31
+ # Convert existing hash to Redis backed hash
32
+ hash = {yin: "yang"}
33
+ redis_hash = Redis::NativeHash.new.update(hash)
34
+
35
+ # Create a hash with a custom key
36
+ hash.key = :custom_key
37
+ hash.key # => :custom_key
38
+
39
+ # Use namespaces
40
+ hash = Redis::NativeHash.new(:custom_namespace)
41
+ hash.namespace # => :custom_namespace
42
+ hash.key # => "20120512212206.376929.5194d9ea37e2d1b6c773b860cce58c7d"
43
+ hash.redis_key # => "custom_namespace:20120512212206.376929.5194d9ea37e2d1b6c773b860cce58c7d"
44
+
45
+ # Initialize with custom namespace and key
46
+ hash = Redis::NativeHash.new(custom_namespace: "my_hash_key")
47
+ hash[:test] = "value"
48
+ hash.namespace # => :custom_namespace
49
+ hash.key # => "my_hash_key"
50
+ hash.redis_key # => "custom_namespace:my_hash_key"
51
+ hash.save # => true
52
+
53
+ # Retrieve existing hash using namespace and key
54
+ existing = Redis::NativeHash.find(custom_namespace: "my_hash_key") # => {"test" => "value"}
55
+ ```
56
+
57
+ ## Example usage for `BigHash`
58
+
59
+ ```ruby
60
+ # Initializing a BigHash
61
+ big = Redis::BigHash.new # => #<Redis::BigHash:0x007fcdfc8890d8 @key=nil, @namespace=nil>
62
+ big = Redis::BigHash.new("custom_key")
63
+ big = Redis::BigHash.new("custom_key", "app_namespace")
64
+
65
+ # No #save method as writes take place immediately
66
+ big = Redis::BigHash("my_key")
67
+ big.[:test] = "right now"
68
+ redis.hget("my_key", "test") # => "right_now"
69
+ ```
70
+
71
+ ## Usage for `LazyHash`
72
+
73
+ A simple lazy-loading proxy object that should behave identically to NativeHash.
74
+ Check `hash.loaded?` if you need to know whether the underlying hash has been read.
75
+
76
+ ## Using as a Rails session store
77
+
78
+ Change your `config/initializers/session_store.rb` to look something like this:
79
+ ```ruby
80
+ require "redis_hash"
81
+ MyAppName::Application.config.session_store :redis_hash
82
+ ```
83
+
84
+ ## Using Redis string-based caching implementation
85
+
86
+ Only the string based implementation is able to properly handle automatic expiration, so it is preferred.
87
+
88
+ Add the following line to the appropriate environment config in `config/environments/`
89
+
90
+ ```ruby
91
+ config.cache_store = :redis_store
92
+ ```
93
+
94
+ To set a cache expiration, use a line like this:
95
+
96
+ ```ruby
97
+ config.cache_store = [:redis_store, :expires_in => 24.hours]
98
+ ```
99
+
100
+ ## Client helpers
101
+
102
+ This gem adds a useful `Redis::ClientHelper` module to simplify both connection sharing and using multiple connections.
103
+ Using `Redis::Client.default=` you can set the connection all future instances of `NativeHash`/`BigHash`/`LazyHash`
104
+ will use.
105
+
106
+ ```ruby
107
+ # Changes to the default cascade down, unless class-level defaults have already been set
108
+ Redis::Client.default # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.6)>
109
+ redis = Redis.new(db: 8) # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/8 (Redis v2.4.6)>
110
+ Redis::Client.default = redis
111
+ Redis::Client.default # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/8 (Redis v2.4.6)>
112
+ Redis::BigHash.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/8 (Redis v2.4.6)>
113
+ Redis::BigHash.new.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/8 (Redis v2.4.6)>
114
+ ```
115
+
116
+ The client helper also lets you set the redis connection to use for an entire class, or a single instance.
117
+
118
+ ```ruby
119
+ Redis::BigHash.redis = Redis.new(db: 4)
120
+ Redis::BigHash.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/4 (Redis v2.4.6)>
121
+ Redis::Client.default # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.6)>
122
+ hash = Redis::BigHash.new
123
+ hash.redis = Redis.new(db: 5)
124
+ hash.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/5 (Redis v2.4.6)>
125
+ Redis::BigHash.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/4 (Redis v2.4.6)>
126
+ ```
127
+
128
+ You can include the client helper into your own classes to give your own classes similar behavior.
129
+ ```ruby
130
+ class CustomClass
131
+ include Redis::ClientHelper
132
+ end
133
+ CustomClass.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.6)>
134
+ x = CustomClass.new
135
+ x.redis = Redis.new(db: 3)
136
+ x.redis # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/3 (Redis v2.4.6)>
137
+ ```
138
+
139
+ **Note:** Your own classes will use `Redis::Client.default` unless a class-level or instance-level connection is set.
140
+
141
+ **See tests for more examples.**
142
+
143
+ ## Contributing to redis-native_hash
6
144
 
7
145
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
146
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
@@ -10,10 +148,7 @@ Description goes here.
10
148
  * Start a feature/bugfix branch
11
149
  * Commit and push until you are happy with your contribution
12
150
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
- * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
-
15
- == Copyright
16
151
 
17
- Copyright (c) 2011 Lyconic. See LICENSE.txt for
18
- further details.
152
+ ### Copyright
19
153
 
154
+ Copyright (c) 2011 Lyconic. See LICENSE.txt for further details.
data/TODO.mkd ADDED
@@ -0,0 +1,58 @@
1
+ ## Plan to make a replacement for Rack::Session::Abstract::SessionHash ##
2
+
3
+ ### Rationale ###
4
+
5
+ `SessionHash` attempts to lazy load the session in Rack 1.3 and higher.
6
+ It does so in a way that does not play nice with Redis::NativeHash.
7
+ Specifically, it `#merge!`s the content of the hash returned by the redis
8
+ session store, instead of using that hash as the actual session hash.
9
+ Since the session hash is no longer a `Redis::NativeHash` instance it is unable
10
+ to keep track of what session values have been changed. This results in an
11
+ additional read when the session is saved since NativeHash needs to read from
12
+ redis again to figure out what values have changed.
13
+
14
+ ### Alternatives ###
15
+
16
+ 1. `NativeHash` could be changed to allow for blind writes. There are two major
17
+ downsides to this approach.
18
+
19
+ 1. `NativeHash` would have no way of knowing which keys are already in the redis
20
+ hash without reading from redis a second time. Not reading the hash before
21
+ writing would mean you would have to delete the hash before beginnning to
22
+ write the data to ensure any keys which no longer exist have been removed.
23
+
24
+ 2. This would circumvent NativeHash's built-in support for concurrent writes
25
+ and could lead to strange issues with multiple apps have to write to the
26
+ same session.
27
+
28
+ 2. Leave it be. Nothing is technically "broken", so don't fix it.
29
+
30
+ 1. Session read/write cycles will require 2x the number of complete loads
31
+ of the corresponding redis hash.
32
+
33
+ 2. Creating and destroying `NativeHash` instances could result in needless
34
+ memory usage and garbage collection on every page request.
35
+ The total cost of this is probably pretty small.
36
+
37
+ 3. Just overwrite `Rack::Session::Abstract::ID` and make `env['rack.session']`
38
+ an instance of `Redis::NativeHash`. You'll lose lazy loading, but it will be a
39
+ quick fix to the extra read problem. This probably won't work though because
40
+ other methods on `Abstract::ID` look for methods like `#loaded?`... ok, it will
41
+ work, it will just require several methods of `Abstract::ID` to be overwritten.
42
+ May actually be easier to just replace `Abstract::ID` altogether.
43
+
44
+
45
+ ### Proposal ###
46
+
47
+ Take the time to write a drop in replacement for `SessionHash`.
48
+ This replacement would respond to the special methods added to `SessionHash`
49
+ and will also accomplish similar lazy loading for the underlying `NativeHash`.
50
+
51
+ Attempt to integrate with existing `Rack::Session::Abstract::ID` by overriding
52
+ `ID#prepare_session`.
53
+
54
+ ## Make RedisHashSession support expirations ##
55
+
56
+ That's right, it currently doesn't support expirations.
57
+ Make it happen and test it. Provide a default expiration too, probably.
58
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.1
@@ -0,0 +1,4 @@
1
+ class ActionDispatch::Session::RedisHash < ActionDispatch::Session::AbstractStore
2
+ include ::Redis::RedisHashSession
3
+ end
4
+
@@ -0,0 +1,48 @@
1
+ require 'redis/native_hash'
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+ class RedisHash < Store
6
+ def initialize(*options)
7
+ options = options.extract_options!
8
+ super(options)
9
+ @hash = ::Redis::BigHash.new(options[:key], options[:namespace] || :rails_cache)
10
+ extend Strategy::LocalCache
11
+ end
12
+
13
+ # Reads multiple values from the cache using a single call to the
14
+ # servers for all keys.
15
+ def read_multi(*names)
16
+ @hash[*names]
17
+ end
18
+
19
+ # Clear the entire cache on server. This method should
20
+ # be used with care when shared cache is being used.
21
+ def clear(options = nil)
22
+ @hash.destroy
23
+ end
24
+
25
+ protected
26
+
27
+ # Read an entry from the cache.
28
+ def read_entry(key, options)
29
+ @hash[key]
30
+ end
31
+
32
+ # Write an entry to the cache.
33
+ def write_entry(key, entry, options)
34
+ if options && options[:unless_exist]
35
+ @hash.add(key, entry)
36
+ else
37
+ @hash[key] = entry
38
+ end
39
+ end
40
+
41
+ # Delete an entry from the cache.
42
+ def delete_entry(key, options)
43
+ @hash.delete(key)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,46 @@
1
+ module ActiveSupport
2
+ module Cache
3
+ class RedisStore < Store
4
+ include Redis::ClientHelper
5
+ def initialize(*options)
6
+ options = options.extract_options!
7
+ super(options)
8
+ extend Strategy::LocalCache
9
+ end
10
+
11
+ # Reads multiple values from the cache using a single call to the
12
+ # servers for all keys.
13
+ def read_multi(*names)
14
+ values = redis.mget *names
15
+ values.map{ |v| Redis::Marshal.load(v) }
16
+ end
17
+
18
+ # Clear the entire cache on server. This method should
19
+ # be used with care when shared cache is being used.
20
+ def clear(options = nil)
21
+ redis.flushdb
22
+ end
23
+
24
+ protected
25
+
26
+ # Read an entry from the cache.
27
+ def read_entry(key, options)
28
+ Redis::Marshal.load(redis.get(key))
29
+ end
30
+
31
+ # Write an entry to the cache.
32
+ def write_entry(key, entry, options)
33
+ method = options && options[:unless_exist] ? :setnx : :set
34
+ expires_in = options[:expires_in].to_i
35
+ redis.send(method, key, Redis::Marshal.dump(entry))
36
+ redis.expire(key, expires_in) if expires_in > 0
37
+ end
38
+
39
+ # Delete an entry from the cache.
40
+ def delete_entry(key, options)
41
+ redis.del(key)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -1,34 +1,69 @@
1
- module Rack
2
- module Session
3
- class RedisHash < Abstract::ID
1
+ module Redis::RedisHashSession
4
2
 
5
- def get_session(env, sid)
6
- session = Redis::NativeHash.find session_prefix => sid
7
- unless sid and session
8
- env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
9
- session = Redis::NativeHash.new session_prefix
10
- sid = session.key
11
- end
12
- return [sid, session]
13
- end
3
+ def initialize(app, options = {})
4
+ super
5
+ @expire_after = options[:expire_after] || 60*60 # 60 minutes, default
6
+ end
14
7
 
15
- def set_session(env, session_id, session, options)
16
- if options[:drop]
17
- session.destroy
18
- return false if options[:drop]
19
- end
20
- if options[:renew]
21
- session_id = session.renew_key
22
- end
23
- session.save
24
- return session_id
25
- end
8
+ def get_session(env, sid)
9
+ session = Redis::LazyHash.new session_prefix => sid
10
+ sid = session.key
11
+ return [sid, session]
12
+ end
26
13
 
27
- def session_prefix
28
- :rack_session
29
- end
14
+ def set_session(env, session_id, session, options)
15
+ @expire_after = options[:expire_after] || @expire_after
16
+ unless session.kind_of?(Redis::LazyHash)
17
+ real_session = Redis::LazyHash.new(session_prefix)
18
+ real_session.update(session) if session.kind_of?(Hash)
19
+ real_session.key = session_id unless session_id.nil?
20
+ session = real_session
21
+ end
22
+ if options[:drop]
23
+ session.destroy
24
+ return false if options[:drop]
25
+ end
26
+ if options[:renew]
27
+ session_id = session.renew_key
28
+ end
29
+ session.save
30
+ session.expire @expire_after
31
+ return session_id
32
+ end
30
33
 
34
+ def destroy_session(env, sid, options)
35
+ session = Redis::LazyHash.new( session_prefix => sid )
36
+ unless session.nil?
37
+ options[:renew] ? session.renew_key : session.destroy
38
+ session.key
31
39
  end
32
40
  end
41
+
42
+ def session_prefix
43
+ :rack_session
44
+ end
33
45
  end
34
46
 
47
+ module Rack
48
+ module Session
49
+ module Abstract
50
+ class ID
51
+ # overwrite prepare_session behavior to turn off use of SessionHash
52
+ def prepare_session(env)
53
+ session_was = env[ENV_SESSION_KEY]
54
+ env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
55
+ env[ENV_SESSION_OPTIONS_KEY][:id], env[ENV_SESSION_KEY] = load_session(env)
56
+ env[ENV_SESSION_KEY].merge! session_was if session_was
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ module Rack
64
+ module Session
65
+ class RedisHash < Abstract::ID
66
+ include ::Redis::RedisHashSession
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,105 @@
1
+ require 'securerandom'
2
+
3
+ class Redis
4
+ class BigHash
5
+ include ClientHelper
6
+ include KeyHelper
7
+
8
+ attr_reader :namespace
9
+
10
+ def initialize( key = nil, namespace = nil )
11
+ @key = key
12
+ @namespace = namespace
13
+ super(nil)
14
+ end
15
+
16
+ def [](*hash_keys)
17
+ if hash_keys.one?
18
+ Redis::Marshal.load( redis.hget(redis_key, convert_key(hash_keys.first)) )
19
+ elsif hash_keys.any?
20
+ values = redis.hmget( redis_key, *hash_keys.map{ |k| convert_key(k) } )
21
+ values.map{ |v| Redis::Marshal.load(v) }
22
+ end
23
+ end
24
+
25
+ def []=(hash_key, value)
26
+ redis.hset( redis_key, convert_key(hash_key), Redis::Marshal.dump(value) )
27
+ end
28
+
29
+ # set only if key doesn't already exist
30
+ # equivilent to doing `hash[:key] ||= value`, but more efficient
31
+ def add(hash_key, value)
32
+ redis.hsetnx( redis_key, convert_key(hash_key), Redis::Marshal.dump(value) )
33
+ end
34
+
35
+ def key=(new_key)
36
+ new_key = generate_key if new_key.nil?
37
+ unless @key.nil? || @key == new_key
38
+ self.class.copy_hash( redis_key, redis_key(new_key) )
39
+ clear
40
+ end
41
+ @key = new_key
42
+ end
43
+
44
+ def namespace=(new_namespace)
45
+ unless new_namespace == namespace
46
+ self.class.copy_hash( redis_key, redis_key(key, new_namespace) )
47
+ clear
48
+ @namespace = new_namespace
49
+ end
50
+ end
51
+
52
+ def keys
53
+ self.class.keys redis_key
54
+ end
55
+
56
+ def key?(hash_key)
57
+ keys.include?(convert_key(hash_key))
58
+ end
59
+ alias_method :include?, :key?
60
+ alias_method :has_key?, :key?
61
+ alias_method :member?, :key?
62
+
63
+ def size
64
+ redis.hlen redis_key
65
+ end
66
+ alias_method :count, :size
67
+ alias_method :length, :size
68
+
69
+ def update(other_hash)
70
+ writes = []
71
+ other_hash.each_pair do |hash_key, value|
72
+ writes << hash_key.to_s
73
+ writes << Redis::Marshal.dump( value )
74
+ end
75
+ redis.hmset(redis_key, *writes)
76
+ end
77
+ alias_method :merge, :update
78
+ alias_method :merge!, :update
79
+
80
+ def delete(hash_key)
81
+ current_value = self[hash_key]
82
+ redis.hdel( redis_key, hash_key )
83
+ current_value
84
+ end
85
+
86
+ def clear
87
+ redis.del redis_key
88
+ end
89
+ alias_method :destroy, :clear
90
+
91
+ class << self
92
+ def keys(redis_key)
93
+ redis.hkeys redis_key
94
+ end
95
+
96
+ def copy_hash(source_key, dest_key)
97
+ keys(source_key).each do |k|
98
+ redis.hset( dest_key, k,
99
+ redis.hget(source_key, k) )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
@@ -0,0 +1,35 @@
1
+ class Redis
2
+ class Client
3
+ def self.default
4
+ @@default_connection ||= ::Redis.new
5
+ end
6
+ def self.default=(connection)
7
+ @@default_connection = connection
8
+ end
9
+ end
10
+ module ClientHelper
11
+ def self.included(base)
12
+ base.send(:extend, ClassMethods)
13
+ base.send(:include, InstanceMethods)
14
+ end
15
+ module InstanceMethods
16
+ def redis
17
+ @redis ||= self.class.redis
18
+ end
19
+ def redis=(connection)
20
+ @redis = connection
21
+ end
22
+ end
23
+ module ClassMethods
24
+ def redis
25
+ unless self.class_variable_defined?(:'@@redis')
26
+ self.class_variable_set(:'@@redis', ::Redis::Client.default)
27
+ end
28
+ self.class_variable_get(:'@@redis')
29
+ end
30
+ def redis=(connection)
31
+ self.class_variable_set(:'@@redis', connection)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ class Redis
2
+ module KeyHelper
3
+
4
+ def key
5
+ @key ||= generate_key
6
+ end
7
+
8
+ def generate_key
9
+ t = Time.now
10
+ t.strftime('%Y%m%d%H%M%S.') + t.usec.to_s.rjust(6,'0') + '.' + SecureRandom.hex(16)
11
+ end
12
+
13
+ def redis_key(key = nil, namespace = nil)
14
+ key ||= self.key
15
+ namespace ||= self.namespace
16
+ namespace.nil? ? key : "#{namespace}:#{key}"
17
+ end
18
+
19
+ def convert_key(key)
20
+ key.to_s
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,64 @@
1
+ require 'forwardable'
2
+
3
+ class Redis
4
+ class LazyHash
5
+ extend Forwardable
6
+ def_delegators :@hash, :key, :namespace, :namespace=, :destroy,
7
+ :reload, :reload!, :expire
8
+
9
+ def initialize(args = nil)
10
+ @hash = NativeHash.new(args)
11
+ @loaded = false
12
+ end
13
+
14
+ def method_missing(meth, *args, &block)
15
+ if @hash.respond_to?(meth)
16
+ self.class.send(:define_method, meth) do |*args, &block|
17
+ lazy_load!
18
+ @hash.send(meth, *args, &block)
19
+ end
20
+ send(meth, *args, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def inspect
27
+ lazy_load!
28
+ @hash.inspect
29
+ end
30
+
31
+ def save
32
+ @hash.save if loaded?
33
+ end
34
+
35
+ def loaded?
36
+ @loaded
37
+ end
38
+
39
+ def to_hash
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ def lazy_load!
46
+ unless loaded?
47
+ reload!
48
+ @hash.retrack!
49
+ @loaded = true
50
+ end
51
+ end
52
+
53
+ class << self
54
+ def find(args)
55
+ case args
56
+ when Hash
57
+ self.new(args)
58
+ when String,Symbol
59
+ self.new(nil=>args)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/redis/marshal.rb CHANGED
@@ -7,11 +7,12 @@ class Redis
7
7
  when Fixnum
8
8
  value
9
9
  else
10
- ::Marshal.dump(value)
10
+ ::Marshal.dump(value) rescue nil
11
11
  end
12
12
  end
13
13
 
14
14
  def self.load(value)
15
+ return nil if value.nil?
15
16
  return value unless value.start_with?("\004")
16
17
  ::Marshal.load(value) rescue value
17
18
  end