9 Commits

7 changed files with 257 additions and 127 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2010 Jim Myhrberg. Copyright (c) 2011 Jim Myhrberg.
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View File

@@ -79,7 +79,7 @@ When retrieving statistics for a given date range, Redistat figures out how to d
## License and Copyright ## License and Copyright
Copyright (c) 2010 Jim Myhrberg. Copyright (c) 2011 Jim Myhrberg.
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View File

@@ -2,97 +2,7 @@ module Redistat
class Finder class Finder
include Database include Database
attr_reader :options
def initialize(options = {})
@options = options
end
def db
super(@options[:connection_ref])
end
def valid_options?
return true if !@options[:scope].blank? && !@options[:label].blank? && !@options[:from].blank? && !@options[:till].blank?
false
end
def find(options = {})
@options.merge!(options)
raise InvalidOptions.new if !valid_options?
if @options[:interval].nil? || !@options[:interval]
find_by_magic
else
find_by_interval
end
end
def find_by_interval(options = {})
@options.merge!(options)
raise InvalidOptions.new if !valid_options?
key = build_key
col = Collection.new(@options)
col.total = Result.new(@options)
build_date_sets.each do |set|
set[:add].each do |date|
result = Result.new
result.date = Date.new(date).to_time
db.hgetall("#{key.prefix}#{date}").each do |k, v|
result[k] = v
col.total.set_or_incr(k, v.to_i)
end
col << result
end
end
col
end
def find_by_magic(options = {})
@options.merge!(options)
raise InvalidOptions.new if !valid_options?
key = Key.new(@options[:scope], @options[:label])
col = Collection.new(@options)
col.total = Result.new(@options)
col << col.total
build_date_sets.each do |set|
sum = Result.new
sum = summarize_add_keys(set[:add], key, sum)
sum = summarize_rem_keys(set[:rem], key, sum)
sum.each do |k, v|
col.total.set_or_incr(k, v.to_i)
end
end
col
end
def build_date_sets
Finder::DateSet.new(@options[:from], @options[:till], @options[:depth], @options[:interval])
end
def build_key
Key.new(@options[:scope], @options[:label])
end
def summarize_add_keys(sets, key, sum)
sets.each do |date|
db.hgetall("#{key.prefix}#{date}").each do |k, v|
sum.set_or_incr(k, v.to_i)
end
end
sum
end
def summarize_rem_keys(sets, key, sum)
sets.each do |date|
db.hgetall("#{key.prefix}#{date}").each do |k, v|
sum.set_or_incr(k, -v.to_i)
end
end
sum
end
class << self class << self
def find(*args) def find(*args)
new.find(*args) new.find(*args)
end end
@@ -126,15 +36,49 @@ module Redistat
def interval(unit) def interval(unit)
new.interval(unit) new.interval(unit)
end end
end
attr_reader :options
def initialize(options = {})
@options = options
end
def all(reload = false)
@result = nil if reload
@result ||= find
end
def total
all.total
end
def each(&block)
all.each(&block)
end
def map(&block)
all.map(&block)
end
def each_with_index(&block)
all.each_with_index(&block)
end
def connection_ref(ref)
reset! if @options[:connection_ref] != ref
@options[:connection_ref] = ref
self
end end
def scope(scope) def scope(scope)
reset! if @options[:scope] != scope
@options[:scope] = scope @options[:scope] = scope
self self
end end
def label(label) def label(label)
reset! if @options[:label] != label
@options[:label] = label @options[:label] = label
self self
end end
@@ -145,25 +89,124 @@ module Redistat
alias :date :dates alias :date :dates
def from(date) def from(date)
reset! if @options[:from] != date
@options[:from] = date @options[:from] = date
self self
end end
def till(date) def till(date)
reset! if @options[:till] != date
@options[:till] = date @options[:till] = date
self self
end end
alias :until :till alias :until :till
def depth(unit) def depth(unit)
reset! if @options[:depth] != unit
@options[:depth] = unit @options[:depth] = unit
self self
end end
def interval(unit) def interval(unit)
reset! if @options[:interval] != unit
@options[:interval] = unit @options[:interval] = unit
self self
end end
def find(options = {})
set_options(options)
raise InvalidOptions.new if !valid_options?
if @options[:interval].nil? || !@options[:interval]
find_by_magic
else
find_by_interval
end
end
private
def set_options(opts = {})
opts = opts.clone
opts.each do |key, value|
self.send(key, opts.delete(key)) if self.respond_to?(key)
end
@options.merge!(opts)
end
def find_by_interval(options = {})
raise InvalidOptions.new if !valid_options?
key = build_key
col = Collection.new(@options)
col.total = Result.new(@options)
build_date_sets.each do |set|
set[:add].each do |date|
result = Result.new
result.date = Date.new(date).to_time
db.hgetall("#{key.prefix}#{date}").each do |k, v|
result[k] = v
col.total.set_or_incr(k, v.to_i)
end
col << result
end
end
col
end
def find_by_magic(options = {})
raise InvalidOptions.new if !valid_options?
key = Key.new(@options[:scope], @options[:label])
col = Collection.new(@options)
col.total = Result.new(@options)
col << col.total
build_date_sets.each do |set|
sum = Result.new
sum = summarize_add_keys(set[:add], key, sum)
sum = summarize_rem_keys(set[:rem], key, sum)
sum.each do |k, v|
col.total.set_or_incr(k, v.to_i)
end
end
col
end
def reset!
@result = nil
end
def valid_options?
return true if !@options[:scope].blank? && !@options[:label].blank? && !@options[:from].blank? && !@options[:till].blank?
false
end
def build_date_sets
Finder::DateSet.new(@options[:from], @options[:till], @options[:depth], @options[:interval])
end
def build_key
Key.new(@options[:scope], @options[:label])
end
def summarize_add_keys(sets, key, sum)
sets.each do |date|
db.hgetall("#{key.prefix}#{date}").each do |k, v|
sum.set_or_incr(k, v.to_i)
end
end
sum
end
def summarize_rem_keys(sets, key, sum)
sets.each do |date|
db.hgetall("#{key.prefix}#{date}").each do |k, v|
sum.set_or_incr(k, -v.to_i)
end
end
sum
end
def db
super(@options[:connection_ref])
end
end end
end end

View File

@@ -6,31 +6,36 @@ module Redistat
base.extend(self) base.extend(self)
end end
#
# statistics store/fetch methods
#
def store(label, stats = {}, date = nil, meta = {}, opts = {}) def store(label, stats = {}, date = nil, meta = {}, opts = {})
Event.new(name, label, date, stats, options.merge(opts), meta).save Event.new(name, label, date, stats, options.merge(opts), meta).save
end end
alias :event :store alias :event :store
def fetch(label, from, till, opts = {})
find(label, from, till, opts).all
end
alias :lookup :fetch
def find(label, from, till, opts = {})
Finder.new( { :scope => name,
:label => label,
:from => from,
:till => till }.merge(options.merge(opts)) )
end
#
# options methods
#
def connect_to(opts = {}) def connect_to(opts = {})
Connection.create(opts.merge(:ref => name)) Connection.create(opts.merge(:ref => name))
options[:connection_ref] = name options[:connection_ref] = name
end end
def connection
db(options[:connection_ref])
end
alias :redis :connection
def fetch(label, from, till, opts = {})
Finder.find({
:scope => name,
:label => label,
:from => from,
:till => till
}.merge(options.merge(opts)))
end
alias :lookup :fetch
def hashed_label(boolean = nil) def hashed_label(boolean = nil)
if !boolean.nil? if !boolean.nil?
options[:hashed_label] = boolean options[:hashed_label] = boolean
@@ -64,15 +69,22 @@ module Redistat
end end
end end
#
# resource access methods
#
def connection
db(options[:connection_ref])
end
alias :redis :connection
def options def options
@options ||= {} @options ||= {}
end end
private
def name def name
options[:class_name] || (@name ||= self.to_s) options[:class_name] || (@name ||= self.to_s)
end end
end end
end end

View File

@@ -1,3 +1,3 @@
module Redistat module Redistat
VERSION = "0.1.0" VERSION = "0.1.1"
end end

View File

@@ -19,6 +19,10 @@ describe Redistat::Finder do
finder = Redistat::Finder.new(options) finder = Redistat::Finder.new(options)
finder.options.should == options finder.options.should == options
finder = Redistat::Finder.new
finder.send(:set_options, options)
finder.options.should == options
finder = Redistat::Finder.dates(two_hours_ago, one_hour_ago).scope("PageViews").label("Label").depth(:hour).interval(:hour) finder = Redistat::Finder.dates(two_hours_ago, one_hour_ago).scope("PageViews").label("Label").depth(:hour).interval(:hour)
finder.options.should == options finder.options.should == options
@@ -85,6 +89,65 @@ describe Redistat::Finder do
lambda { Redistat::Finder.find(:from => 3.hours.ago) }.should raise_error(Redistat::InvalidOptions) lambda { Redistat::Finder.find(:from => 3.hours.ago) }.should raise_error(Redistat::InvalidOptions)
end end
describe "Lazy-Loading" do
before(:each) do
@first_stat, @last_stat = create_example_stats
@finder = Redistat::Finder.new
@finder.from(@first_stat).till(@last_stat).scope(@scope).label(@label).depth(:hour)
@match = [{}, {"visitors"=>"4", "views"=>"6"},
{"visitors"=>"2", "views"=>"3"},
{"visitors"=>"2", "views"=>"3"}, {}]
end
it "should lazy-load" do
@finder.instance_variable_get("@result").should be_nil
stats = @finder.all
@finder.instance_variable_get("@result").should_not be_nil
stats.should == @finder.find # find method directly fetches results
stats.total.should == @finder.total
stats.total.should == { "views" => 12, "visitors" => 8 }
stats.total.from.should == @first_stat
stats.total.till.should == @last_stat
stats.first.should == stats.total
@finder.all.object_id.should == stats.object_id
@finder.from(@first_stat + 2.hours)
@finder.instance_variable_get("@result").should be_nil
@finder.all.object_id.should_not == stats.object_id
stats = @finder.all
stats.total.should == { "views" => 6, "visitors" => 4 }
end
it "should handle #map" do
@finder.interval(:hour)
@finder.map { |r| r }.should == @match
end
it "should handle #each" do
@finder.interval(:hour)
res = []
@finder.each { |r| res << r }
res.should == @match
end
it "should handle #each_with_index" do
@finder.interval(:hour)
res = {}
match = {}
@finder.each_with_index { |r, i| res[i] = r }
@match.each_with_index { |r, i| match[i] = r }
res.should == match
end
end
# helper methods # helper methods
@@ -93,7 +156,7 @@ describe Redistat::Finder do
Redistat::Summary.update(key, @stats, :hour) Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 13:53")) key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 13:53"))
Redistat::Summary.update(key, @stats, :hour) Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 14:32")) key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 14:52"))
Redistat::Summary.update(key, @stats, :hour) Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02"))) key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02")))
Redistat::Summary.update(key, @stats, :hour) Redistat::Summary.update(key, @stats, :hour)

View File

@@ -5,6 +5,7 @@ describe Redistat::Model do
include Redistat::Database include Redistat::Database
before(:each) do before(:each) do
@time = Time.utc(2010, 8, 28, 12, 0, 0)
ModelHelper1.redis.flushdb ModelHelper1.redis.flushdb
ModelHelper2.redis.flushdb ModelHelper2.redis.flushdb
ModelHelper3.redis.flushdb ModelHelper3.redis.flushdb
@@ -16,6 +17,17 @@ describe Redistat::Model do
ModelHelper2.send(:name).should == "ModelHelper2" ModelHelper2.send(:name).should == "ModelHelper2"
end end
it "should return a Finder" do
two_hours_ago = 2.hours.ago
one_hour_ago = 1.hour.ago
finder = ModelHelper1.find('label', two_hours_ago, one_hour_ago)
finder.should be_a(Redistat::Finder)
finder.options[:scope].should == 'ModelHelper1'
finder.options[:label].should == 'label'
finder.options[:from].should == two_hours_ago
finder.options[:till].should == one_hour_ago
end
it "should listen to model-defined options" do it "should listen to model-defined options" do
ModelHelper2.depth.should == :day ModelHelper2.depth.should == :day
ModelHelper2.store_event.should == true ModelHelper2.store_event.should == true
@@ -43,28 +55,28 @@ describe Redistat::Model do
end end
it "should store and fetch stats" do it "should store and fetch stats" do
ModelHelper1.store("sheep.black", {:count => 6, :weight => 461}, 4.hours.ago) ModelHelper1.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4))
ModelHelper1.store("sheep.black", {:count => 2, :weight => 156}) ModelHelper1.store("sheep.black", {:count => 2, :weight => 156}, @time)
stats = ModelHelper1.fetch("sheep.black", 2.hours.ago, 1.hour.from_now) stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(2), @time.hours_since(1))
stats.total["count"].should == 2 stats.total["count"].should == 2
stats.total["weight"].should == 156 stats.total["weight"].should == 156
stats.first.should == stats.total stats.first.should == stats.total
stats = ModelHelper1.fetch("sheep.black", 5.hours.ago, 1.hour.from_now) stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total[:count].should == 8 stats.total[:count].should == 8
stats.total[:weight].should == 617 stats.total[:weight].should == 617
stats.first.should == stats.total stats.first.should == stats.total
ModelHelper1.store("sheep.white", {:count => 5, :weight => 393}, 4.hours.ago) ModelHelper1.store("sheep.white", {:count => 5, :weight => 393}, @time.hours_ago(4))
ModelHelper1.store("sheep.white", {:count => 4, :weight => 316}) ModelHelper1.store("sheep.white", {:count => 4, :weight => 316}, @time)
stats = ModelHelper1.fetch("sheep.white", 2.hours.ago, 1.hour.from_now) stats = ModelHelper1.fetch("sheep.white", @time.hours_ago(2), @time.hours_since(1))
stats.total[:count].should == 4 stats.total[:count].should == 4
stats.total[:weight].should == 316 stats.total[:weight].should == 316
stats.first.should == stats.total stats.first.should == stats.total
stats = ModelHelper1.fetch("sheep.white", 5.hours.ago, 1.hour.from_now) stats = ModelHelper1.fetch("sheep.white", @time.hours_ago(5), @time.hours_since(1))
stats.total[:count].should == 9 stats.total[:count].should == 9
stats.total[:weight].should == 709 stats.total[:weight].should == 709
stats.first.should == stats.total stats.first.should == stats.total
@@ -73,31 +85,31 @@ describe Redistat::Model do
it "should connect to different Redis servers on a per-model basis" do it "should connect to different Redis servers on a per-model basis" do
ModelHelper3.redis.client.db.should == 14 ModelHelper3.redis.client.db.should == 14
ModelHelper3.store("sheep.black", {:count => 6, :weight => 461}, 4.hours.ago) ModelHelper3.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4))
ModelHelper3.store("sheep.black", {:count => 2, :weight => 156}) ModelHelper3.store("sheep.black", {:count => 2, :weight => 156}, @time)
db.keys("*").should be_empty db.keys("*").should be_empty
ModelHelper1.redis.keys("*").should be_empty ModelHelper1.redis.keys("*").should be_empty
db("ModelHelper3").keys("*").should have(5).items db("ModelHelper3").keys("*").should have(5).items
ModelHelper3.redis.keys("*").should have(5).items ModelHelper3.redis.keys("*").should have(5).items
stats = ModelHelper3.fetch("sheep.black", 2.hours.ago, 1.hour.from_now) stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(2), @time.hours_since(1))
stats.total["count"].should == 2 stats.total["count"].should == 2
stats.total["weight"].should == 156 stats.total["weight"].should == 156
stats = ModelHelper3.fetch("sheep.black", 5.hours.ago, 1.hour.from_now) stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total[:count].should == 8 stats.total[:count].should == 8
stats.total[:weight].should == 617 stats.total[:weight].should == 617
ModelHelper3.connect_to(:port => 8379, :db => 13) ModelHelper3.connect_to(:port => 8379, :db => 13)
ModelHelper3.redis.client.db.should == 13 ModelHelper3.redis.client.db.should == 13
stats = ModelHelper3.fetch("sheep.black", 5.hours.ago, 1.hour.from_now) stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total.should == {} stats.total.should == {}
ModelHelper3.connect_to(:port => 8379, :db => 14) ModelHelper3.connect_to(:port => 8379, :db => 14)
ModelHelper3.redis.client.db.should == 14 ModelHelper3.redis.client.db.should == 14
stats = ModelHelper3.fetch("sheep.black", 5.hours.ago, 1.hour.from_now) stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total[:count].should == 8 stats.total[:count].should == 8
stats.total[:weight].should == 617 stats.total[:weight].should == 617
end end