Browse files

initial import

  • Loading branch information...
0 parents commit f6d90edff0f62360c8be32eba5d34c73ec939907 @igrigorik committed May 9, 2010
Showing with 175 additions and 0 deletions.
  1. +83 −0 rrstat.rb
  2. +92 −0 spec/rrstat_spec.rb
83 rrstat.rb
@@ -0,0 +1,83 @@
+require 'redis'
+
+class RRStat
+ def initialize(opts)
+ @precision = opts[:precision]
+ @buckets = opts[:buckets]
+
+ @current = nil
+ @db = Redis.new
+ end
+
+ def time_epoch; Time.now.to_i / @buckets; end
+
+ def buckets(set)
+ (0...@buckets).inject([]) {|a,v| a.push "#{set}:#{(time_epoch - v)}" }
+ end
+
+ def epoch(set)
+ e = time_epoch
+ now = set + ":" + e.to_s
+
+ if now != @current
+ debug [:new_epoch, e]
+ @current = now
+
+ clear_bucket("#{set}:#{e - @buckets}")
+ end
+
+ @current
+ end
+
+ def union_epochs(set)
+ debug [:union_epochs, buckets(set)]
+ @db.zunion("#{set}:union", buckets(set))
+ end
+
+ def score(set, key)
+ union_epochs(set)
+
+ buckets(set).each {|b| debug [b, @db.zscore(b, key)]}
+ @db.zscore("#{set}:union", key).to_i
+ end
+
+ def incr(set, key, val=1)
+ debug [:zincrby, epoch(set), val, key]
+ @db.zincrby(epoch(set), val, key).to_i
+ end
+
+ def first(set, num, options = {})
+ union_epochs(set)
+ e = @db.zrevrange("#{set}:union", 0, num, options)
+ options.key?(:with_scores) ? Hash[*e] : e
+ end
+
+ def last(set, num, options = {})
+ union_epochs(set)
+ e = @db.zrange("#{set}:union", 0, num, options)
+ options.key?(:with_scores) ? Hash[*e] : e
+ end
+
+ def delete(set, key)
+ buckets(set).each do |b|
+ @db.zrem(b, key)
+ end
+ end
+
+ def clear(set)
+ buckets(set).each do |b|
+ clear_bucket(b)
+ end
+ end
+
+ def flushdb; @db.flushdb; end
+
+ private
+
+ def clear_bucket(b)
+ debug [:clearing_epoch, b]
+ @db.zremrangebyrank(b, 0, 2**32)
+ end
+
+ def debug(msg); p msg; end
+end
92 spec/rrstat_spec.rb
@@ -0,0 +1,92 @@
+require 'spec'
+require 'delorean'
+require 'rrstat'
+require 'time'
+
+describe RRStat do
+ include Delorean
+
+ let(:rr) { RRStat.new(:precision => 10, :buckets => 6) }
+
+ before(:each) { time_travel_to("Jan 1 2010") }
+ before(:each) { rr.flushdb }
+
+ it "should initialize db" do
+ lambda {
+ RRStat.new(:precision => 10, :buckets => 6)
+ }.should_not raise_error
+ end
+
+ it "should return score of item" do
+ rr.score("test", "random_key").should == 0
+ end
+
+ it "should increment buckets within correct epoch" do
+ rr.epoch("test").should match(/test:210394200/)
+
+ rr.incr("test", "key")
+ rr.score("test", "key").should == 1
+
+ rr.incr("test", "key", 2)
+ rr.score("test", "key").should == 3
+
+ # advance to next epoch
+ time_travel_to(Time.now + 10) do
+ rr.epoch("test").should match(/test:210394201/)
+
+ rr.incr("test", "key")
+ rr.score("test", "key").should == 4
+ end
+
+ # advance 5 epochs, to scroll original incr's off the list
+ time_travel_to(Time.now + 40) do
+ rr.epoch("test").should match(/test:210394206/)
+
+ rr.incr("test", "key")
+ rr.score("test", "key").should == 2
+ end
+ end
+
+ it "should return top N items from all epochs" do
+ rr.incr("test", "key1", 1)
+ rr.incr("test", "key2", 3)
+
+ # advance to next epoch
+ time_travel_to(Time.now + 10) do
+ rr.epoch("test").should match(/test:210394201/)
+ rr.incr("test", "key3", 5)
+
+ rr.first("test", 3).should == ["key3", "key2", "key1"]
+ rr.first("test", 3, :with_scores => true).should == {"key3"=>"5", "key2"=>"3", "key1"=>"1"}
+ end
+ end
+
+ it "should return last N items from all epochs" do
+ rr.incr("test", "key1", 1)
+ rr.incr("test", "key2", 3)
+
+ # advance to next epoch
+ time_travel_to(Time.now + 10) do
+ rr.epoch("test").should match(/test:210394201/)
+ rr.incr("test", "key3", 5)
+
+ rr.last("test", 3).should == ["key1", "key2", "key3"]
+ rr.last("test", 3, :with_scores => true).should == {"key1"=>"1", "key2"=>"3", "key3"=>"5"}
+ end
+ end
+
+ it "should erase key from all epochs" do
+ rr.incr("test", "key", 1)
+ rr.score("test", "key").should == 1
+
+ # advance to next epoch
+ time_travel_to(Time.now + 10) do
+ rr.epoch("test").should match(/test:210394201/)
+ rr.incr("test", "key")
+ rr.score("test", "key").should == 2
+
+ rr.delete("test", "key")
+ rr.score("test", "key").should == 0
+ end
+ end
+end

0 comments on commit f6d90ed

Please sign in to comment.