Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f6d90ed
Showing
2 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |