Skip to content

Commit

Permalink
initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
igrigorik committed May 9, 2010
0 parents commit f6d90ed
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 0 deletions.
83 changes: 83 additions & 0 deletions 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 changes: 92 additions & 0 deletions 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.