fresh_redis 0.0.4 → 0.0.5
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.
- data/Gemfile.lock +3 -1
- data/fresh_redis.gemspec +1 -0
- data/lib/fresh_redis/hash.rb +24 -15
- data/lib/fresh_redis/string.rb +2 -2
- data/lib/fresh_redis/version.rb +1 -1
- data/lib/fresh_redis.rb +22 -1
- data/spec/fresh_redis/hash_spec.rb +79 -25
- data/spec/fresh_redis/key_spec.rb +0 -1
- data/spec/fresh_redis/string_spec.rb +15 -14
- data/spec/fresh_redis_spec.rb +22 -0
- metadata +24 -2
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fresh_redis (0.0.
|
4
|
+
fresh_redis (0.0.5)
|
5
5
|
redis
|
6
6
|
|
7
7
|
GEM
|
@@ -16,6 +16,7 @@ GEM
|
|
16
16
|
rspec (~> 2.11)
|
17
17
|
listen (0.5.3)
|
18
18
|
mock_redis (0.5.2)
|
19
|
+
rake (0.9.2.2)
|
19
20
|
redis (3.0.2)
|
20
21
|
rspec (2.11.0)
|
21
22
|
rspec-core (~> 2.11.0)
|
@@ -34,4 +35,5 @@ DEPENDENCIES
|
|
34
35
|
fresh_redis!
|
35
36
|
guard-rspec (= 2.1.0)
|
36
37
|
mock_redis (= 0.5.2)
|
38
|
+
rake (= 0.9.2.2)
|
37
39
|
rspec
|
data/fresh_redis.gemspec
CHANGED
data/lib/fresh_redis/hash.rb
CHANGED
@@ -1,38 +1,47 @@
|
|
1
1
|
class FreshRedis
|
2
2
|
module Hash
|
3
|
+
|
3
4
|
def fhset(key, hash_key, value, options={})
|
4
|
-
key =
|
5
|
+
key = build_key(key, options)
|
5
6
|
@redis.multi do
|
6
|
-
@redis.hset(key.redis_key, hash_key, value)
|
7
|
+
@redis.hset(key.redis_key, hash_key, n(value))
|
7
8
|
@redis.expire(key.redis_key, key.freshness)
|
8
9
|
end
|
9
10
|
end
|
10
11
|
|
11
12
|
def fhget(key, hash_key, options={})
|
12
|
-
key =
|
13
|
-
|
14
|
-
|
13
|
+
key = build_key(key, options)
|
14
|
+
|
15
|
+
bucket_values = @redis.pipelined {
|
16
|
+
key.timestamp_buckets.reverse.each do |bucket_key|
|
15
17
|
@redis.hget(bucket_key, hash_key)
|
16
18
|
end
|
17
|
-
}
|
19
|
+
}
|
20
|
+
|
21
|
+
# find the first non-nil value
|
22
|
+
most_recent_value = bucket_values.find{|e| e }
|
23
|
+
|
24
|
+
un_n(most_recent_value)
|
18
25
|
end
|
19
26
|
|
20
27
|
def fhgetall(key, options={})
|
21
|
-
key =
|
22
|
-
|
28
|
+
key = build_key(key, options)
|
29
|
+
|
30
|
+
bucket_values = @redis.pipelined {
|
23
31
|
key.timestamp_buckets.each do |bucket_key|
|
24
32
|
@redis.hgetall(bucket_key)
|
25
33
|
end
|
26
|
-
}
|
34
|
+
}
|
35
|
+
|
36
|
+
merged_values = bucket_values.inject({}){ |acc, bucket_hash|
|
37
|
+
acc.merge(bucket_hash)
|
38
|
+
}
|
39
|
+
|
40
|
+
merged_values.reject{ |key, value| n?(value) }
|
27
41
|
end
|
28
42
|
|
29
43
|
def fhdel(key, hash_key, options={})
|
30
|
-
key
|
31
|
-
@redis.pipelined do
|
32
|
-
key.timestamp_buckets.each do |bucket_key|
|
33
|
-
@redis.hdel(bucket_key, hash_key)
|
34
|
-
end
|
35
|
-
end
|
44
|
+
fhset(key, hash_key, nil, options)
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
data/lib/fresh_redis/string.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class FreshRedis
|
2
2
|
module String
|
3
3
|
def fincr(key, options={})
|
4
|
-
key =
|
4
|
+
key = build_key(key, options)
|
5
5
|
@redis.multi do
|
6
6
|
@redis.incr(key.redis_key)
|
7
7
|
@redis.expire(key.redis_key, key.freshness)
|
@@ -9,7 +9,7 @@ class FreshRedis
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def fsum(key, options={})
|
12
|
-
key =
|
12
|
+
key = build_key(key, options)
|
13
13
|
@redis.pipelined {
|
14
14
|
key.timestamp_buckets.each do |bucket_key|
|
15
15
|
@redis.get(bucket_key)
|
data/lib/fresh_redis/version.rb
CHANGED
data/lib/fresh_redis.rb
CHANGED
@@ -7,7 +7,28 @@ class FreshRedis
|
|
7
7
|
include Hash
|
8
8
|
include String
|
9
9
|
|
10
|
-
|
10
|
+
NIL_VALUE = "__FR_NIL__"
|
11
|
+
|
12
|
+
def initialize(redis, options={})
|
11
13
|
@redis = redis
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_key(base_key, options={})
|
18
|
+
options = @options.merge(options)
|
19
|
+
Key.build(base_key, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def n(value)
|
24
|
+
value || NIL_VALUE
|
25
|
+
end
|
26
|
+
|
27
|
+
def un_n(value)
|
28
|
+
n?(value) ? nil : value
|
29
|
+
end
|
30
|
+
|
31
|
+
def n?(value)
|
32
|
+
value == NIL_VALUE
|
12
33
|
end
|
13
34
|
end
|
@@ -6,53 +6,107 @@ describe FreshRedis do
|
|
6
6
|
let(:mock_redis) { MockRedis.new }
|
7
7
|
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
8
8
|
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
9
|
+
let(:normalized_one_minute_ago) { Time.new(2012, 9, 27, 15, 39, 0, "+10:00").to_i }
|
10
|
+
let(:normalized_two_minutes_ago) { Time.new(2012, 9, 27, 15, 38, 0, "+10:00").to_i }
|
11
|
+
let(:normalized_old) { Time.new(2012, 9, 27, 14, 38, 0, "+10:00").to_i }
|
9
12
|
|
10
13
|
context "hash keys" do
|
11
14
|
|
12
15
|
describe "#fhset" do
|
13
|
-
it "
|
14
|
-
subject.fhset "foo", "bar", "value", :
|
15
|
-
subject.fhset "foo", "bar", "newer_value", :
|
16
|
-
subject.fhset "foo", "bar", "different_bucket", :
|
16
|
+
it "sets a value for a key in a hash for the normalized timestamp" do
|
17
|
+
subject.fhset "foo", "bar", "value", :t => now - 3
|
18
|
+
subject.fhset "foo", "bar", "newer_value", :t => now
|
19
|
+
subject.fhset "foo", "bar", "different_bucket", :t => now - 60 # different normalized key
|
20
|
+
|
17
21
|
mock_redis.data["foo:#{normalized_now_minute}"].should == {"bar" => "newer_value"}
|
18
22
|
end
|
19
23
|
|
20
|
-
it "
|
24
|
+
it "sets a placeholder value if nil is set as the value" do
|
25
|
+
subject.fhset "foo", "bar", nil, :t => now
|
26
|
+
|
27
|
+
mock_redis.data["foo:#{normalized_now_minute}"].should == {"bar" => FreshRedis::NIL_VALUE }
|
28
|
+
end
|
29
|
+
|
30
|
+
it "sets the freshness as the expiry" do
|
21
31
|
# relying on mock_redis's time handling here - which converts to/from using Time.now Possible flakey temporal breakage potential
|
22
32
|
subject.fhset "foo", "bar", "baz", :freshness => 3600, :t => now
|
33
|
+
|
23
34
|
mock_redis.ttl("foo:#{normalized_now_minute}").should == 3600
|
24
35
|
end
|
25
36
|
end
|
26
37
|
|
27
38
|
describe "#fhdel" do
|
28
|
-
it "
|
29
|
-
subject.fhset "foo", "bar", "value", :
|
30
|
-
subject.fhset "foo", "bar", "different_bucket", :
|
31
|
-
|
32
|
-
subject.
|
39
|
+
it "sets the value for a key in a hash for the normalized timestamp to be placeholder nil" do
|
40
|
+
subject.fhset "foo", "bar", "value", :t => now
|
41
|
+
subject.fhset "foo", "bar", "different_bucket", :t => now - 60
|
42
|
+
|
43
|
+
subject.fhdel "foo", "bar", :t => now # Should only change the most recent bucket
|
44
|
+
|
45
|
+
mock_redis.data["foo:#{normalized_now_minute}"].should == { "bar" => FreshRedis::NIL_VALUE }
|
46
|
+
mock_redis.data["foo:#{normalized_one_minute_ago}"].should == { "bar" => "different_bucket" }
|
33
47
|
end
|
34
48
|
end
|
35
49
|
|
36
50
|
describe "#fhget" do
|
37
|
-
it "
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
subject.fhget("
|
51
|
+
it "gets the most recent value for the field across timestamped buckets" do
|
52
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "notbar", "francis"
|
53
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "bar", "bill"
|
54
|
+
mock_redis.hset "foo:#{normalized_two_minutes_ago}", "bar", "louis"
|
55
|
+
|
56
|
+
subject.fhget("foo", "bar", :t => now).should == "bill"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns nil if the most recent value is the nil placeholder" do
|
60
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "notbar", "francis"
|
61
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "bar", FreshRedis::NIL_VALUE
|
62
|
+
mock_redis.hset "foo:#{normalized_two_minutes_ago}", "bar", "louis"
|
63
|
+
|
64
|
+
subject.fhget("foo", "bar", :t => now).should be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns the most recent value if a nil placeholder value in an earlier bucket has been overwritten in a later bucket" do
|
68
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "bar", "francis"
|
69
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "bar", FreshRedis::NIL_VALUE
|
70
|
+
mock_redis.hset "foo:#{normalized_two_minutes_ago}", "bar", "louis"
|
71
|
+
|
72
|
+
subject.fhget("foo", "bar", :t => now).should == "francis"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns nil if value is not found" do
|
76
|
+
subject.fhget("foo", "bar", :t => now).should be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns nil if the value is in a bucket that has expired" do
|
80
|
+
# this should be handled by redis expiry anyway, but verify code is behaving as expected and not querying more data than needed
|
81
|
+
mock_redis.hset "foo:#{normalized_old}", "bar", "louis"
|
82
|
+
subject.fhget("foo", "bar", :t => now).should be_nil
|
43
83
|
end
|
44
84
|
end
|
45
85
|
|
46
86
|
describe "#fhgetall" do
|
47
|
-
it "
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
subject.fhgetall("
|
53
|
-
|
54
|
-
|
55
|
-
|
87
|
+
it "merges the values for all keys across timestamp buckets" do
|
88
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "bar", "francis"
|
89
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "baz", "zoey"
|
90
|
+
mock_redis.hset "foo:#{normalized_two_minutes_ago}", "boz", "louis"
|
91
|
+
|
92
|
+
subject.fhgetall("foo", :t => now).should == { "bar" => "francis", "baz" => "zoey", "boz" => "louis" }
|
93
|
+
end
|
94
|
+
|
95
|
+
it "removes keys that have a nil placeholder value as the most recent value" do
|
96
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "bar", FreshRedis::NIL_VALUE
|
97
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "bar", "zoey"
|
98
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "baz", "louis"
|
99
|
+
|
100
|
+
subject.fhgetall("foo", :t => now).should == { "baz" => "louis" }
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns the most recent value if a nil placeholder value in an earlier bucket has been overwritten in a later bucket" do
|
104
|
+
mock_redis.hset "foo:#{normalized_now_minute}", "bar", "francis"
|
105
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "bar", FreshRedis::NIL_VALUE
|
106
|
+
mock_redis.hset "foo:#{normalized_two_minutes_ago}", "bar", "louis"
|
107
|
+
mock_redis.hset "foo:#{normalized_one_minute_ago}", "baz", "bill"
|
108
|
+
|
109
|
+
subject.fhgetall("foo", :t => now).should == { "bar" => "francis", "baz" => "bill" }
|
56
110
|
end
|
57
111
|
end
|
58
112
|
end
|
@@ -3,7 +3,6 @@ require 'fresh_redis'
|
|
3
3
|
describe FreshRedis::Key do
|
4
4
|
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
5
5
|
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
6
|
-
let(:normalized_now_hour) { Time.new(2012, 9, 27, 15, 0, 0, "+10:00").to_i }
|
7
6
|
|
8
7
|
describe ".build" do
|
9
8
|
it "complains if no args" do
|
@@ -10,9 +10,9 @@ describe FreshRedis do
|
|
10
10
|
context "string keys" do
|
11
11
|
describe "#fincr" do
|
12
12
|
it "should increment the key for the normalized timestamp" do
|
13
|
-
subject.fincr "foo", :
|
14
|
-
subject.fincr "foo", :
|
15
|
-
subject.fincr "foo", :
|
13
|
+
subject.fincr "foo", :t => now
|
14
|
+
subject.fincr "foo", :t => now + 3
|
15
|
+
subject.fincr "foo", :t => now + 60 # different normalized key
|
16
16
|
mock_redis.data["foo:#{normalized_now_minute}"].to_i.should == 2
|
17
17
|
end
|
18
18
|
|
@@ -24,19 +24,20 @@ describe FreshRedis do
|
|
24
24
|
end
|
25
25
|
|
26
26
|
describe "#fsum" do
|
27
|
+
subject{ FreshRedis.new(mock_redis, :granularity => 10, :freshness => 60) }
|
27
28
|
it "should add the values of keys for specified freshness and granularity" do
|
28
|
-
subject.fincr "foo", :
|
29
|
-
subject.fincr "foo", :
|
30
|
-
subject.fincr "foo", :
|
31
|
-
subject.fincr "foo", :
|
32
|
-
subject.fincr "foo", :
|
33
|
-
subject.fincr "foo", :
|
34
|
-
subject.fincr "foo", :
|
35
|
-
subject.fincr "foo", :
|
36
|
-
subject.fincr "foo", :
|
37
|
-
subject.fincr "foo", :
|
29
|
+
subject.fincr "foo", :t => now - 60 - 10
|
30
|
+
subject.fincr "foo", :t => now - 60 + 1
|
31
|
+
subject.fincr "foo", :t => now - 60 + 2
|
32
|
+
subject.fincr "foo", :t => now - 60 + 3
|
33
|
+
subject.fincr "foo", :t => now - 60 + 5
|
34
|
+
subject.fincr "foo", :t => now - 60 + 8
|
35
|
+
subject.fincr "foo", :t => now - 60 + 13
|
36
|
+
subject.fincr "foo", :t => now - 60 + 21
|
37
|
+
subject.fincr "foo", :t => now - 60 + 34
|
38
|
+
subject.fincr "foo", :t => now - 60 + 55
|
38
39
|
|
39
|
-
subject.fsum("foo", :
|
40
|
+
subject.fsum("foo", :t => now).should == 9
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
data/spec/fresh_redis_spec.rb
CHANGED
@@ -2,4 +2,26 @@ require 'fresh_redis'
|
|
2
2
|
require 'mock_redis'
|
3
3
|
|
4
4
|
describe FreshRedis do
|
5
|
+
let(:mock_redis) { MockRedis.new }
|
6
|
+
|
7
|
+
describe "#build_key" do
|
8
|
+
it "builds a new key based on custom options" do
|
9
|
+
key = "key"
|
10
|
+
FreshRedis::Key.should_receive(:build).with("foo", :granularity => 111, :freshness => 222).and_return(key)
|
11
|
+
|
12
|
+
fresh_redis = FreshRedis.new(mock_redis, :granularity => 111, :freshness => 222)
|
13
|
+
|
14
|
+
fresh_redis.build_key("foo").should == key
|
15
|
+
end
|
16
|
+
|
17
|
+
it "builds a new key no options if custom options not provided" do
|
18
|
+
key = "key"
|
19
|
+
FreshRedis::Key.should_receive(:build).with("foo", {}).and_return(key)
|
20
|
+
|
21
|
+
fresh_redis = FreshRedis.new(mock_redis)
|
22
|
+
|
23
|
+
fresh_redis.build_key("foo").should == key
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
5
27
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fresh_redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -75,6 +75,22 @@ dependencies:
|
|
75
75
|
- - '='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 2.1.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - '='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.9.2.2
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - '='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.9.2.2
|
78
94
|
description: Aggregate, expiring, recent data in Redis
|
79
95
|
email:
|
80
96
|
- madlep@madlep.com
|
@@ -112,12 +128,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
128
|
- - ! '>='
|
113
129
|
- !ruby/object:Gem::Version
|
114
130
|
version: '0'
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
hash: -1443868887867383191
|
115
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
135
|
none: false
|
117
136
|
requirements:
|
118
137
|
- - ! '>='
|
119
138
|
- !ruby/object:Gem::Version
|
120
139
|
version: '0'
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
hash: -1443868887867383191
|
121
143
|
requirements: []
|
122
144
|
rubyforge_project:
|
123
145
|
rubygems_version: 1.8.23
|