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 CHANGED
@@ -1,15 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fresh_redis (0.0.1)
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.1)
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
@@ -0,0 +1,6 @@
1
+ guard 'rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
- # FreshRedis
1
+ # fresh\_redis
2
2
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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
@@ -19,4 +19,5 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_development_dependency 'rspec'
21
21
  gem.add_development_dependency 'mock_redis', '0.5.2'
22
+ gem.add_development_dependency 'guard-rspec', '2.1.0'
22
23
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  class FreshRedis
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/fresh_redis.rb CHANGED
@@ -1,61 +1,13 @@
1
- require 'fresh_redis/timestamp'
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 Timestamp
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
@@ -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.1
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-09-27 00:00:00.000000000Z
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: &70193817243680 !ruby/object:Gem::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: *70193817243680
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: &70193817243180 !ruby/object:Gem::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: *70193817243180
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: &70193817242380 !ruby/object:Gem::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: *70193817242380
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/timestamp.rb
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/timestamp_spec.rb
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.10
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/timestamp_spec.rb
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