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 +143 -8
- data/TODO.mkd +58 -0
- data/VERSION +1 -1
- data/lib/action_dispatch/session/redis_hash.rb +4 -0
- data/lib/active_support/cache/redis_hash.rb +48 -0
- data/lib/active_support/cache/redis_store.rb +46 -0
- data/lib/rack/session/redis_hash.rb +61 -26
- data/lib/redis/big_hash.rb +105 -0
- data/lib/redis/client_helper.rb +35 -0
- data/lib/redis/key_helper.rb +24 -0
- data/lib/redis/lazy_hash.rb +64 -0
- data/lib/redis/marshal.rb +2 -1
- data/lib/redis/native_hash.rb +93 -106
- data/lib/redis/tracked_hash.rb +20 -9
- data/lib/redis_hash.rb +22 -0
- data/redis-native_hash.gemspec +20 -9
- data/spec/redis/big_hash_spec.rb +118 -0
- data/spec/redis/lazy_hash_spec.rb +160 -0
- data/spec/redis/{redis_hash_spec.rb → native_hash_spec.rb} +2 -1
- data/spec/spec_helper.rb +3 -1
- data/spec/user_defined/user_spec.rb +5 -1
- metadata +26 -17
data/README.mkd
CHANGED
@@ -1,8 +1,146 @@
|
|
1
|
-
|
1
|
+
# redis-native_hash
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
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
|
1
|
+
0.2.1
|
@@ -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
|
2
|
-
module Session
|
3
|
-
class RedisHash < Abstract::ID
|
1
|
+
module Redis::RedisHashSession
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|