fresh_redis 0.0.1 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +11 -2
- data/Guardfile +6 -0
- data/README.md +47 -3
- data/fresh_redis.gemspec +1 -0
- data/lib/fresh_redis/hash.rb +38 -0
- data/lib/fresh_redis/key.rb +56 -0
- data/lib/fresh_redis/string.rb +22 -0
- data/lib/fresh_redis/version.rb +1 -1
- data/lib/fresh_redis.rb +5 -53
- data/spec/fresh_redis/hash_spec.rb +60 -0
- data/spec/fresh_redis/key_spec.rb +64 -0
- data/spec/fresh_redis/string_spec.rb +44 -0
- data/spec/fresh_redis_spec.rb +0 -35
- metadata +51 -19
- data/lib/fresh_redis/timestamp.rb +0 -17
- data/spec/fresh_redis/timestamp_spec.rb +0 -49
data/Gemfile.lock
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fresh_redis (0.0.
|
4
|
+
fresh_redis (0.0.4)
|
5
5
|
redis
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
10
|
diff-lcs (1.1.3)
|
11
|
+
guard (1.4.0)
|
12
|
+
listen (>= 0.4.2)
|
13
|
+
thor (>= 0.14.6)
|
14
|
+
guard-rspec (2.1.0)
|
15
|
+
guard (>= 1.1)
|
16
|
+
rspec (~> 2.11)
|
17
|
+
listen (0.5.3)
|
11
18
|
mock_redis (0.5.2)
|
12
|
-
redis (3.0.
|
19
|
+
redis (3.0.2)
|
13
20
|
rspec (2.11.0)
|
14
21
|
rspec-core (~> 2.11.0)
|
15
22
|
rspec-expectations (~> 2.11.0)
|
@@ -18,11 +25,13 @@ GEM
|
|
18
25
|
rspec-expectations (2.11.3)
|
19
26
|
diff-lcs (~> 1.1.3)
|
20
27
|
rspec-mocks (2.11.3)
|
28
|
+
thor (0.16.0)
|
21
29
|
|
22
30
|
PLATFORMS
|
23
31
|
ruby
|
24
32
|
|
25
33
|
DEPENDENCIES
|
26
34
|
fresh_redis!
|
35
|
+
guard-rspec (= 2.1.0)
|
27
36
|
mock_redis (= 0.5.2)
|
28
37
|
rspec
|
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# fresh\_redis
|
2
2
|
|
3
|
-
|
3
|
+
Redis is great for managing data that expires on atomically (like caches). However, for data that expires gradually over time, built in commands don't get you all the way.
|
4
|
+
|
5
|
+
For instance, how would you calculate _"count of login failures and successes for the last hour"_? The problem is while you can keep a count using a simple `incr` operation, you have to expire the entire total all at once, or not at all.
|
6
|
+
|
7
|
+
A common solution is to split the data up into buckets, say one for each minute, each with their own expiry. You write to the current bucket, set the expiry, then allow it to naturally expire and drop out of your result set over time. To obtain the total value, you `get` all the bucket values, and aggregate the values in some fashion.
|
8
|
+
|
9
|
+
That's pretty much what fresh\_redis does, except with less boilerplate, and a little more flexibility.
|
4
10
|
|
5
11
|
## Installation
|
6
12
|
|
@@ -18,7 +24,42 @@ Or install it yourself as:
|
|
18
24
|
|
19
25
|
## Usage
|
20
26
|
|
21
|
-
|
27
|
+
### Simple usage
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "redis"
|
31
|
+
require "fresh_redis"
|
32
|
+
fresh = FreshRedis.new(Redis.current)
|
33
|
+
|
34
|
+
fresh.fincr "failed_login"
|
35
|
+
|
36
|
+
# wait a bit...
|
37
|
+
fresh.fincr "failed_login"
|
38
|
+
|
39
|
+
# then straight away...
|
40
|
+
fresh.fincr "failed_login"
|
41
|
+
|
42
|
+
fresh.fsum "failed_login" # will return 3
|
43
|
+
|
44
|
+
# wait for the first incr to expire...
|
45
|
+
fresh.fsum "failed_login" # will return 2, cause the first incr has expired by now
|
46
|
+
```
|
47
|
+
|
48
|
+
### Tweaking _"freshness"_ and _"granularity"_.
|
49
|
+
|
50
|
+
Think of it like stock rotation at your local supermarket. Freshness is how long we'll keep food around for before throwing it out, granularity is what batches we'll throw old food out together as. Something like _"we'll keep food around for a week, but we'll throw out everything for the same day at the same time."_ This is a performance trade off. Smaller granularity means more precise expiration of data, at the expense of having to store, retrieve, and check more buckets of data to get the aggregate value.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# lets track douch users spamming the forum so we can do something about it...
|
54
|
+
|
55
|
+
# store post count for a user for 10 minutes (600 seconds), in buckets of time duration 30 seconds
|
56
|
+
fresh.fincr "recent_posts:#{user.id}", :freshness => 600, :granularity => 30
|
57
|
+
|
58
|
+
# ...
|
59
|
+
|
60
|
+
# note, need to pass in the SAME freshness and granularity options as fincr, so it can correclty lookup the correct keys
|
61
|
+
fresh.fsum "recent_posts:#{user.id}", :freshness => 600, :granularity => 30
|
62
|
+
```
|
22
63
|
|
23
64
|
## Contributing
|
24
65
|
|
@@ -27,3 +68,6 @@ TODO: Write usage instructions here
|
|
27
68
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
69
|
4. Push to the branch (`git push origin my-new-feature`)
|
29
70
|
5. Create new Pull Request
|
71
|
+
|
72
|
+
## Who the hell?
|
73
|
+
I blame [@madlep](http://twitter.com/madlep) aka Julian Doherty. Send hate mail to [madlep@madlep.com](mailto:madlep@madlep.com), or deface [madlep.com](http://madlep.com) in protest
|
data/fresh_redis.gemspec
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
class FreshRedis
|
2
|
+
module Hash
|
3
|
+
def fhset(key, hash_key, value, options={})
|
4
|
+
key = Key.build(key, options)
|
5
|
+
@redis.multi do
|
6
|
+
@redis.hset(key.redis_key, hash_key, value)
|
7
|
+
@redis.expire(key.redis_key, key.freshness)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def fhget(key, hash_key, options={})
|
12
|
+
key = Key.build(key, options)
|
13
|
+
@redis.pipelined {
|
14
|
+
key.timestamp_buckets.each do |bucket_key|
|
15
|
+
@redis.hget(bucket_key, hash_key)
|
16
|
+
end
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
def fhgetall(key, options={})
|
21
|
+
key = Key.build(key, options)
|
22
|
+
@redis.pipelined {
|
23
|
+
key.timestamp_buckets.each do |bucket_key|
|
24
|
+
@redis.hgetall(bucket_key)
|
25
|
+
end
|
26
|
+
}.reject { |hash| hash.count.zero? }
|
27
|
+
end
|
28
|
+
|
29
|
+
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
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class FreshRedis
|
2
|
+
class Key
|
3
|
+
|
4
|
+
DEFAULT_OPTIONS = {
|
5
|
+
:freshness => 60 * 60, # 1 hour
|
6
|
+
:granularity => 1 * 60 # 1 minute
|
7
|
+
}
|
8
|
+
|
9
|
+
def self.build(*args)
|
10
|
+
raise "Don't know how to build FreshRedis::Key for #{args.inspect}" unless args[0]
|
11
|
+
|
12
|
+
return args[0] if Key === args[0] # early exit if we've already got a key
|
13
|
+
|
14
|
+
base_key = args[0]
|
15
|
+
|
16
|
+
options = DEFAULT_OPTIONS.merge(args[1] || {})
|
17
|
+
options[:t] ||= Time.now.to_i
|
18
|
+
|
19
|
+
self.new(base_key, options[:t], options[:freshness], options[:granularity])
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :freshness
|
23
|
+
|
24
|
+
def initialize(base_key, t, freshness, granularity)
|
25
|
+
@base_key = base_key
|
26
|
+
@t = t
|
27
|
+
@freshness = freshness
|
28
|
+
@granularity = granularity
|
29
|
+
end
|
30
|
+
|
31
|
+
def redis_key
|
32
|
+
[@base_key, normalize_time(@t, @granularity)].join(":")
|
33
|
+
end
|
34
|
+
|
35
|
+
def timestamp_buckets
|
36
|
+
from = normalize_time(@t - @freshness, @granularity)
|
37
|
+
to = normalize_time(@t, @granularity)
|
38
|
+
(from..to).step(@granularity).map{|timestamp| [@base_key, timestamp].join(":") }
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
same = true
|
43
|
+
same &= Key === other
|
44
|
+
same &= @base_key == other.instance_variable_get(:@base_key)
|
45
|
+
same &= @t == other.instance_variable_get(:@t)
|
46
|
+
same &= @freshness == other.instance_variable_get(:@freshness)
|
47
|
+
same &= @granularity == other.instance_variable_get(:@granularity)
|
48
|
+
same
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def normalize_time(t, granularity)
|
53
|
+
t - (t % granularity)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class FreshRedis
|
2
|
+
module String
|
3
|
+
def fincr(key, options={})
|
4
|
+
key = Key.build(key, options)
|
5
|
+
@redis.multi do
|
6
|
+
@redis.incr(key.redis_key)
|
7
|
+
@redis.expire(key.redis_key, key.freshness)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def fsum(key, options={})
|
12
|
+
key = Key.build(key, options)
|
13
|
+
@redis.pipelined {
|
14
|
+
key.timestamp_buckets.each do |bucket_key|
|
15
|
+
@redis.get(bucket_key)
|
16
|
+
end
|
17
|
+
}.reduce(0){|acc, value|
|
18
|
+
value ? acc + value.to_i : acc
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/fresh_redis/version.rb
CHANGED
data/lib/fresh_redis.rb
CHANGED
@@ -1,61 +1,13 @@
|
|
1
|
-
require 'fresh_redis/
|
1
|
+
require 'fresh_redis/hash'
|
2
|
+
require 'fresh_redis/key'
|
3
|
+
require 'fresh_redis/string'
|
2
4
|
require 'fresh_redis/version'
|
3
5
|
|
4
6
|
class FreshRedis
|
5
|
-
include
|
6
|
-
|
7
|
-
VERSION = "0.0.1"
|
8
|
-
|
9
|
-
DEFAULT_OPTIONS = {
|
10
|
-
:freshness => 60 * 60, # 1 hour
|
11
|
-
:granularity => 1 * 60 # 1 minute
|
12
|
-
}
|
7
|
+
include Hash
|
8
|
+
include String
|
13
9
|
|
14
10
|
def initialize(redis)
|
15
11
|
@redis = redis
|
16
12
|
end
|
17
|
-
|
18
|
-
def fincr(key, options={})
|
19
|
-
options = default_options(options)
|
20
|
-
t = options[:t]
|
21
|
-
freshness = options[:freshness]
|
22
|
-
granularity = options[:granularity]
|
23
|
-
|
24
|
-
key = normalize_key(key, t, granularity)
|
25
|
-
@redis.multi do
|
26
|
-
@redis.incr key
|
27
|
-
@redis.expire key, freshness
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def fsum(key, options={})
|
32
|
-
options = default_options(options)
|
33
|
-
|
34
|
-
reduce(key, options, 0){|acc, timestamp_total|
|
35
|
-
acc + timestamp_total.to_i
|
36
|
-
}
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
def reduce(key, options={}, initial=nil, &reduce_operation)
|
41
|
-
options = default_options(options)
|
42
|
-
t = options[:t]
|
43
|
-
freshness = options[:freshness]
|
44
|
-
granularity = options[:granularity]
|
45
|
-
|
46
|
-
raw_totals = @redis.pipelined {
|
47
|
-
range_timestamps(t, freshness, granularity).each do |timestamp|
|
48
|
-
timestamp_key = [key, timestamp].join(":")
|
49
|
-
@redis.get(timestamp_key)
|
50
|
-
end
|
51
|
-
}
|
52
|
-
|
53
|
-
raw_totals.reduce(initial, &reduce_operation)
|
54
|
-
end
|
55
|
-
|
56
|
-
def default_options(options)
|
57
|
-
options = DEFAULT_OPTIONS.merge(options)
|
58
|
-
options[:t] ||= Time.now.to_i
|
59
|
-
options
|
60
|
-
end
|
61
13
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'fresh_redis'
|
2
|
+
require 'mock_redis'
|
3
|
+
|
4
|
+
describe FreshRedis do
|
5
|
+
subject{ FreshRedis.new(mock_redis) }
|
6
|
+
let(:mock_redis) { MockRedis.new }
|
7
|
+
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
8
|
+
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
9
|
+
|
10
|
+
context "hash keys" do
|
11
|
+
|
12
|
+
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
|
17
|
+
mock_redis.data["foo:#{normalized_now_minute}"].should == {"bar" => "newer_value"}
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should set the freshness as the expiry" do
|
21
|
+
# relying on mock_redis's time handling here - which converts to/from using Time.now Possible flakey temporal breakage potential
|
22
|
+
subject.fhset "foo", "bar", "baz", :freshness => 3600, :t => now
|
23
|
+
mock_redis.ttl("foo:#{normalized_now_minute}").should == 3600
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
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"]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
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"]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
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
|
+
]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'fresh_redis'
|
2
|
+
|
3
|
+
describe FreshRedis::Key do
|
4
|
+
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
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
|
+
|
8
|
+
describe ".build" do
|
9
|
+
it "complains if no args" do
|
10
|
+
expect { FreshRedis::Key.build() }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "just returns the key if a FreshRedis::Key is provided" do
|
14
|
+
key = FreshRedis::Key.new("key", 123, 456, 789)
|
15
|
+
FreshRedis::Key.build(key).should == key
|
16
|
+
end
|
17
|
+
|
18
|
+
it "constructs a FreshRedis::Key with the provided options" do
|
19
|
+
key = FreshRedis::Key.build("key", :t => 123, :freshness => 456, :granularity => 789)
|
20
|
+
key.should == FreshRedis::Key.new("key", 123, 456, 789)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
it "constructs a FreshRedis::Key with the default options" do
|
25
|
+
key = FreshRedis::Key.build("key")
|
26
|
+
key.should == FreshRedis::Key.new(
|
27
|
+
"key",
|
28
|
+
Time.now.to_i,
|
29
|
+
FreshRedis::Key::DEFAULT_OPTIONS[:freshness],
|
30
|
+
FreshRedis::Key::DEFAULT_OPTIONS[:granularity]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#redis_key" do
|
37
|
+
it "should append the normalized timestamp to the key" do
|
38
|
+
FreshRedis::Key.build("foo", :t => now, :granularity => 60).redis_key.should == "foo:#{normalized_now_minute}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#timestamp_buckets" do
|
43
|
+
let(:buckets) { FreshRedis::Key.build("foo", :t => now, :freshness => 600, :granularity => 60).timestamp_buckets }
|
44
|
+
it "generates an enumerable over the range" do
|
45
|
+
buckets.should be_kind_of(Enumerable)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "has one timestamp bucket for each granularity step in the fresh range" do
|
49
|
+
buckets.count.should == 11 # fence-posting. we include the first and last elements in a timestamp range split by granularity
|
50
|
+
end
|
51
|
+
|
52
|
+
it "has the first timestamp as the maximum freshness" do
|
53
|
+
buckets.first.should == ["foo", normalized_now_minute - 600].join(":")
|
54
|
+
end
|
55
|
+
|
56
|
+
it "has now as the maximum freshness" do
|
57
|
+
buckets.to_a.last.should == ["foo", normalized_now_minute].join(":")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "steps through the normalized timestamps split up by granularity" do
|
61
|
+
buckets.each_with_index{|b, i| b.should == ["foo", normalized_now_minute - 600 + i * 60].join(":") }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'fresh_redis'
|
2
|
+
require 'mock_redis'
|
3
|
+
|
4
|
+
describe FreshRedis do
|
5
|
+
subject{ FreshRedis.new(mock_redis) }
|
6
|
+
let(:mock_redis) { MockRedis.new }
|
7
|
+
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
8
|
+
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
9
|
+
|
10
|
+
context "string keys" do
|
11
|
+
describe "#fincr" do
|
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
|
16
|
+
mock_redis.data["foo:#{normalized_now_minute}"].to_i.should == 2
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should set the freshness as the expiry" do
|
20
|
+
# relying on mock_redis's time handling here - which converts to/from using Time.now Possible flakey temporal breakage potential
|
21
|
+
subject.fincr "foo", :freshness => 3600, :t => now
|
22
|
+
mock_redis.ttl("foo:#{normalized_now_minute}").should == 3600
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#fsum" do
|
27
|
+
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
|
38
|
+
|
39
|
+
subject.fsum("foo", :freshness => 60, :granularity => 10, :t => now).should == 9
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/spec/fresh_redis_spec.rb
CHANGED
@@ -2,39 +2,4 @@ require 'fresh_redis'
|
|
2
2
|
require 'mock_redis'
|
3
3
|
|
4
4
|
describe FreshRedis do
|
5
|
-
subject{ FreshRedis.new(mock_redis) }
|
6
|
-
let(:mock_redis) { MockRedis.new }
|
7
|
-
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
8
|
-
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
9
|
-
|
10
|
-
describe "#fincr" do
|
11
|
-
it "should increment the key for the normalized timestamp" do
|
12
|
-
subject.fincr "foo", :granularity => 60, :t => now
|
13
|
-
subject.fincr "foo", :granularity => 60, :t => now + 3
|
14
|
-
subject.fincr "foo", :granularity => 60, :t => now + 60 # different normalized key
|
15
|
-
mock_redis.data["foo:#{normalized_now_minute}"].to_i.should == 2
|
16
|
-
end
|
17
|
-
|
18
|
-
it "should set the freshness as the expiry" do
|
19
|
-
# relying on mock_redis's time handling here - which converts to/from using Time.now Possible flakey temporal breakage potential
|
20
|
-
subject.fincr "foo", :freshness => 3600, :t => now
|
21
|
-
mock_redis.ttl("foo:#{normalized_now_minute}").should == 3600
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
describe "#fsum" do
|
26
|
-
it "should add the values of keys for specified freshness and granularity" do
|
27
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 1
|
28
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 2
|
29
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 3
|
30
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 5
|
31
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 8
|
32
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 13
|
33
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 21
|
34
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 34
|
35
|
-
subject.fincr "foo", :freshness => 60, :granularity => 10, :t => now - 60 + 55
|
36
|
-
|
37
|
-
subject.fsum("foo", :freshness => 60, :granularity => 10, :t => now).should == 9
|
38
|
-
end
|
39
|
-
end
|
40
5
|
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.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: rspec
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,18 +37,44 @@ dependencies:
|
|
32
37
|
version: '0'
|
33
38
|
type: :development
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: mock_redis
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
|
-
- - =
|
51
|
+
- - '='
|
42
52
|
- !ruby/object:Gem::Version
|
43
53
|
version: 0.5.2
|
44
54
|
type: :development
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.5.2
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: guard-rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.1.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - '='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.1.0
|
47
78
|
description: Aggregate, expiring, recent data in Redis
|
48
79
|
email:
|
49
80
|
- madlep@madlep.com
|
@@ -55,14 +86,19 @@ files:
|
|
55
86
|
- .rspec
|
56
87
|
- Gemfile
|
57
88
|
- Gemfile.lock
|
89
|
+
- Guardfile
|
58
90
|
- LICENSE
|
59
91
|
- README.md
|
60
92
|
- Rakefile
|
61
93
|
- fresh_redis.gemspec
|
62
94
|
- lib/fresh_redis.rb
|
63
|
-
- lib/fresh_redis/
|
95
|
+
- lib/fresh_redis/hash.rb
|
96
|
+
- lib/fresh_redis/key.rb
|
97
|
+
- lib/fresh_redis/string.rb
|
64
98
|
- lib/fresh_redis/version.rb
|
65
|
-
- spec/fresh_redis/
|
99
|
+
- spec/fresh_redis/hash_spec.rb
|
100
|
+
- spec/fresh_redis/key_spec.rb
|
101
|
+
- spec/fresh_redis/string_spec.rb
|
66
102
|
- spec/fresh_redis_spec.rb
|
67
103
|
homepage: ''
|
68
104
|
licenses: []
|
@@ -76,25 +112,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
112
|
- - ! '>='
|
77
113
|
- !ruby/object:Gem::Version
|
78
114
|
version: '0'
|
79
|
-
segments:
|
80
|
-
- 0
|
81
|
-
hash: 107948125749487774
|
82
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
116
|
none: false
|
84
117
|
requirements:
|
85
118
|
- - ! '>='
|
86
119
|
- !ruby/object:Gem::Version
|
87
120
|
version: '0'
|
88
|
-
segments:
|
89
|
-
- 0
|
90
|
-
hash: 107948125749487774
|
91
121
|
requirements: []
|
92
122
|
rubyforge_project:
|
93
|
-
rubygems_version: 1.8.
|
123
|
+
rubygems_version: 1.8.23
|
94
124
|
signing_key:
|
95
125
|
specification_version: 3
|
96
126
|
summary: Use redis for working with recent temporal based data that can expiry gradually.
|
97
127
|
Useful for things like "get a count all failed login attempts for the last hour"
|
98
128
|
test_files:
|
99
|
-
- spec/fresh_redis/
|
129
|
+
- spec/fresh_redis/hash_spec.rb
|
130
|
+
- spec/fresh_redis/key_spec.rb
|
131
|
+
- spec/fresh_redis/string_spec.rb
|
100
132
|
- spec/fresh_redis_spec.rb
|
@@ -1,17 +0,0 @@
|
|
1
|
-
class FreshRedis
|
2
|
-
module Timestamp
|
3
|
-
def normalize_key(key, t, granularity)
|
4
|
-
[key, normalize_time(t, granularity)].join(":")
|
5
|
-
end
|
6
|
-
|
7
|
-
def normalize_time(t, granularity)
|
8
|
-
t - (t % granularity)
|
9
|
-
end
|
10
|
-
|
11
|
-
def range_timestamps(t, freshness, granularity)
|
12
|
-
from = normalize_time(t - freshness, granularity)
|
13
|
-
to = normalize_time(t, granularity)
|
14
|
-
(from..to).step(granularity)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
require 'fresh_redis'
|
2
|
-
|
3
|
-
describe FreshRedis::Timestamp do
|
4
|
-
subject { Class.new.extend(FreshRedis::Timestamp) }
|
5
|
-
let(:now) { Time.new(2012, 9, 27, 15, 40, 56, "+10:00").to_i }
|
6
|
-
let(:normalized_now_minute) { Time.new(2012, 9, 27, 15, 40, 0, "+10:00").to_i }
|
7
|
-
let(:normalized_now_hour) { Time.new(2012, 9, 27, 15, 0, 0, "+10:00").to_i }
|
8
|
-
|
9
|
-
describe "#normalize_key" do
|
10
|
-
it "should append the normalized timestamp to the key" do
|
11
|
-
subject.normalize_key("foo", now, 60).should == "foo:#{normalized_now_minute}"
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
describe "#normalize_time" do
|
16
|
-
it "should round down timestamp to nearest multiple of granularity" do
|
17
|
-
subject.normalize_time(now, 60).should == normalized_now_minute
|
18
|
-
subject.normalize_time(now, 3600).should == normalized_now_hour
|
19
|
-
end
|
20
|
-
|
21
|
-
it "shouldn't change the timestamp if the granularity is 1" do
|
22
|
-
subject.normalize_time(now, 1).should == now
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
describe "#range_timestamps" do
|
27
|
-
let(:range) { subject.range_timestamps(now, 600, 60) }
|
28
|
-
it "should generate an enumerable over the range" do
|
29
|
-
range.should be_kind_of(Enumerable)
|
30
|
-
end
|
31
|
-
|
32
|
-
it "should have one timestamp for each granularity step in the fresh range" do
|
33
|
-
range.count.should == 11 # fence-posting. we include the first and last elements in a timestamp range split by granularity
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should have the first timestamp as the maximum freshness" do
|
37
|
-
range.first.should == normalized_now_minute - 600
|
38
|
-
end
|
39
|
-
|
40
|
-
it "should have now as the maximum freshness" do
|
41
|
-
range.to_a.last.should == normalized_now_minute
|
42
|
-
end
|
43
|
-
|
44
|
-
it "should step through the normalized timestamps split up by granularity" do
|
45
|
-
range.each_with_index{|t, i| t.should == normalized_now_minute - 600 + i * 60 }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|