fresh_redis 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fresh_redis (0.0.4)
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
@@ -20,4 +20,5 @@ Gem::Specification.new do |gem|
20
20
  gem.add_development_dependency 'rspec'
21
21
  gem.add_development_dependency 'mock_redis', '0.5.2'
22
22
  gem.add_development_dependency 'guard-rspec', '2.1.0'
23
+ gem.add_development_dependency 'rake', '0.9.2.2'
23
24
  end
@@ -1,38 +1,47 @@
1
1
  class FreshRedis
2
2
  module Hash
3
+
3
4
  def fhset(key, hash_key, value, options={})
4
- key = Key.build(key, options)
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 = Key.build(key, options)
13
- @redis.pipelined {
14
- key.timestamp_buckets.each do |bucket_key|
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
- }.compact
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 = Key.build(key, options)
22
- @redis.pipelined {
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
- }.reject { |hash| hash.count.zero? }
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 = Key.build(key, options)
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
@@ -1,7 +1,7 @@
1
1
  class FreshRedis
2
2
  module String
3
3
  def fincr(key, options={})
4
- key = Key.build(key, options)
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 = Key.build(key, options)
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)
@@ -1,3 +1,3 @@
1
1
  class FreshRedis
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
data/lib/fresh_redis.rb CHANGED
@@ -7,7 +7,28 @@ class FreshRedis
7
7
  include Hash
8
8
  include String
9
9
 
10
- def initialize(redis)
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 "should set a value for a key in a hash for the normalized timestamp" do
14
- subject.fhset "foo", "bar", "value", :granularity => 60, :t => now
15
- subject.fhset "foo", "bar", "newer_value", :granularity => 60, :t => now + 3
16
- subject.fhset "foo", "bar", "different_bucket", :granularity => 60, :t => now + 60 # different normalized key
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 "should set the freshness as the expiry" do
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 "should remove a value for a key in a hash for the normalized timestamp" do
29
- subject.fhset "foo", "bar", "value", :granularity => 10, :freshness => 20, :t => now - 15
30
- subject.fhset "foo", "bar", "different_bucket", :granularity => 10, :freshness => 20, :t => now
31
- subject.fhdel "foo", "bar", :granularity => 10, :freshness => 0, :t => now # Should only delete the most recent bucket
32
- subject.fhget("foo", "bar", :granularity => 10, :freshness => 20, :t => now ).should == ["value"]
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 "should get all the values of the specified key in specified hash for specified freshness and granularity" do
38
- subject.fhset "requests", "some_key", "0", :freshness => 60, :granularity => 10, :t => now - 60 - 10 # Too old of a bucket
39
- subject.fhset "requests", "some_key", "1", :freshness => 60, :granularity => 10, :t => now - 60 + 5
40
- subject.fhset "requests", "some_key", "2", :freshness => 60, :granularity => 10, :t => now - 60 + 15
41
- subject.fhset "requests", "some_key", "3", :freshness => 60, :granularity => 10, :t => now - 60 + 16 # This overwrites the previous value in the bucket
42
- subject.fhget("requests", "some_key", :freshness => 60, :granularity => 10, :t => now).should == ["1", "3"]
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 "should get all the values of the specified hash for specified freshness and granularity" do
48
- subject.fhset "requests", "some_key", "0", :freshness => 60, :granularity => 10, :t => now - 60 - 10 # Too old of a bucket
49
- subject.fhset "requests", "some_key", "1", :freshness => 60, :granularity => 10, :t => now - 60 + 5
50
- subject.fhset "requests", "some_key", "2", :freshness => 60, :granularity => 10, :t => now - 60 + 15
51
- subject.fhset "requests", "another_key", "3", :freshness => 60, :granularity => 10, :t => now - 60 + 16 # This overwrites the previous value in the bucket
52
- subject.fhgetall("requests", :freshness => 60, :granularity => 10, :t => now).should == [
53
- {"some_key" => "1"},
54
- {"some_key" => "2", "another_key" => "3"}
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", :granularity => 60, :t => now
14
- subject.fincr "foo", :granularity => 60, :t => now + 3
15
- subject.fincr "foo", :granularity => 60, :t => now + 60 # different normalized key
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", :freshness => 60, :granularity => 10, :t => now - 60 - 10
29
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 1
30
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 2
31
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 3
32
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 5
33
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 8
34
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 13
35
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 21
36
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 34
37
- subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 55
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", :freshness => 60, :granularity => 10, :t => now).should == 9
40
+ subject.fsum("foo", :t => now).should == 9
40
41
  end
41
42
  end
42
43
 
@@ -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
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-14 00:00:00.000000000 Z
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