35 Commits

Author SHA1 Message Date
dc3816f691 Merge branch 'release/v0.1.1' 2011-03-09 01:34:51 +00:00
57d8fddd23 started release v0.1.1 2011-03-09 01:34:12 +00:00
c5e9c02c84 Merge branch 'feature/lazy-loading' into dev 2011-03-09 01:31:34 +00:00
e0eac61a59 updated Model to take advantage of new lazy-loading features 2011-03-09 01:31:20 +00:00
33e9477552 finalized lazy-loading work on Finder 2011-03-09 01:25:09 +00:00
7e8e1dacc7 initial work and specs to properly support lazy-loading results from Finder objects 2011-03-08 01:30:48 +00:00
06cd30a20c fixed Model spec as it was failing at certain times of the day 2011-03-08 01:03:56 +00:00
f481554fc9 updated copyright year 2011-03-08 00:44:10 +00:00
a9b6f2c99a Merge branch 'release/v0.1.0' into dev 2011-03-04 17:41:45 +00:00
8710f4a51f Merge branch 'release/v0.1.0' 2011-03-04 17:41:39 +00:00
96e9b0a736 Version bump to 0.1.0 2011-03-04 17:41:14 +00:00
102fb41a6b Merge branch 'feature/grouping' into dev 2011-03-04 17:40:17 +00:00
b0a44a6abc some more sanity checks to Label spec 2011-03-04 17:40:02 +00:00
f8dfb034af added label grouping to Key and Summary classes 2011-03-04 17:39:51 +00:00
15904e8a94 added grouping support to Redistat::Label 2011-03-04 16:25:31 +00:00
fe221c3f31 added enable_grouping option to disable grouping features, enabled by default 2011-03-04 13:02:20 +00:00
7b1feda061 added key grouping for statistics Hash
Example:
store(“message”, {“count/private” => 1})
store(“message”, {“count/public” => 1})
fetch("message", 2.minutes.ago, Time.now)
  #=> { "count" => 2,
        "count/private" => 1,
        "count/public" => 1 }
2011-03-04 12:54:50 +00:00
968ef47ac5 fixed name of SystemTimer gem in readme 2011-03-04 12:17:55 +00:00
e3c4a5da9a added Gemfile.lock to .gitignore file 2011-03-04 12:17:39 +00:00
b215c6d56c made rcov rake task work 2011-03-04 12:16:49 +00:00
8d5c73a539 Merge branch 'release/v0.0.9' into dev 2011-02-23 18:11:54 +00:00
0d5170bc26 Merge branch 'release/v0.0.9' 2011-02-23 18:11:50 +00:00
4692780d1e Version bump to 0.0.9 2011-02-23 18:11:15 +00:00
f8ec626762 removed Gemfile.lock from git repo (this is a gem
not an application ^_^)
2011-02-23 18:10:09 +00:00
ec54385192 updated activesupport dependency to ">= 2.3.6" as
redistat breaks with earlier versions
2011-02-23 18:09:19 +00:00
4808a97d19 added .rvmrc file for development ease 2011-02-23 18:08:21 +00:00
1dce2780e0 Merge branch 'Oscil8-master' into dev 2011-02-23 18:00:16 +00:00
Ariel Salomon
cab7ed5633 Fix for use with Active Support 2.3.x 2011-02-23 07:42:58 -08:00
861d040187 Merge branch 'release/v0.0.8' into dev 2011-01-12 16:13:18 +00:00
3267ee1eb9 Merge branch 'release/v0.0.8' 2011-01-12 16:13:13 +00:00
6309e4b217 Version bump to 0.0.8 2011-01-12 16:12:55 +00:00
776ee8ac97 make version available in code via
Redistat::VERSION
2011-01-12 16:11:19 +00:00
3b346e88e0 moved DateHelper module to it's own files for the
sake of transparency
2011-01-12 16:10:38 +00:00
c3fe861b10 connection handling was so thread-safe that it
stopped working in newly created threads
2011-01-12 16:04:42 +00:00
bc5034b6bb Merge branch 'release/v0.0.7' into dev 2010-12-29 17:29:20 +00:00
26 changed files with 496 additions and 195 deletions

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ rdoc
pkg/* pkg/*
*.gem *.gem
.bundle .bundle
Gemfile.lock
## PROJECT::SPECIFIC ## PROJECT::SPECIFIC
.yardoc/* .yardoc/*

1
.rvmrc Normal file
View File

@@ -0,0 +1 @@
rvm gemset use redistat

View File

@@ -1,41 +0,0 @@
PATH
remote: .
specs:
redistat (0.0.6)
activesupport (>= 2.3.0)
json (>= 1.4.0)
redis (>= 2.1.0)
time_ext (>= 0.2.8)
GEM
remote: http://rubygems.org/
specs:
activesupport (3.0.3)
diff-lcs (1.1.2)
i18n (0.4.2)
json (1.4.6)
redis (2.1.1)
rspec (2.1.0)
rspec-core (~> 2.1.0)
rspec-expectations (~> 2.1.0)
rspec-mocks (~> 2.1.0)
rspec-core (2.1.0)
rspec-expectations (2.1.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.1.0)
time_ext (0.2.8)
activesupport (>= 2.3.0)
i18n (>= 0.4.2)
yard (0.6.3)
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 2.3.0)
json (>= 1.4.0)
redis (>= 2.1.0)
redistat!
rspec (>= 2.1.0)
time_ext (>= 0.2.8)
yard (>= 0.6.3)

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

@@ -14,7 +14,7 @@ Redis fits perfectly with all of these requirements. It has atomic operations li
gem install redistat gem install redistat
If you are using Ruby 1.8.x, it's recommended you also install the `system_timer` gem, as the Redis gem will otherwise complain. If you are using Ruby 1.8.x, it's recommended you also install the `SystemTimer` gem, as the Redis gem will otherwise complain.
## Usage ## Usage
@@ -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

@@ -14,6 +14,7 @@ end
RSpec::Core::RakeTask.new(:rcov) do |spec| RSpec::Core::RakeTask.new(:rcov) do |spec|
spec.pattern = 'spec/**/*_spec.rb' spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true spec.rcov = true
spec.rcov_opts = ['--exclude', 'spec']
end end
task :default => [:start, :spec, :stop] task :default => [:start, :spec, :stop]

View File

@@ -1,7 +1,7 @@
require 'rubygems' require 'rubygems'
require 'active_support' require 'active_support'
require 'active_support/hash_with_indifferent_access' if !Hash.respond_to?(:with_indifferent_access) # Active Support 2.x and 3.x require 'active_support/hash_with_indifferent_access' if !{}.respond_to?(:with_indifferent_access) # Active Support 2.x and 3.x
require 'redis' require 'redis'
require 'date' require 'date'
require 'time' require 'time'
@@ -13,6 +13,7 @@ require 'redistat/collection'
require 'redistat/connection' require 'redistat/connection'
require 'redistat/database' require 'redistat/database'
require 'redistat/date' require 'redistat/date'
require 'redistat/date_helper'
require 'redistat/event' require 'redistat/event'
require 'redistat/finder' require 'redistat/finder'
require 'redistat/finder/date_set' require 'redistat/finder/date_set'
@@ -22,6 +23,7 @@ require 'redistat/model'
require 'redistat/result' require 'redistat/result'
require 'redistat/scope' require 'redistat/scope'
require 'redistat/summary' require 'redistat/summary'
require 'redistat/version'
require 'redistat/core_ext/date' require 'redistat/core_ext/date'
require 'redistat/core_ext/time' require 'redistat/core_ext/time'
@@ -34,6 +36,7 @@ module Redistat
KEY_EVENT = ".event:" KEY_EVENT = ".event:"
KEY_LEBELS = "Redistat.lables:" KEY_LEBELS = "Redistat.lables:"
KEY_EVENT_IDS = ".event_ids" KEY_EVENT_IDS = ".event_ids"
GROUP_SEPARATOR = "/"
class InvalidOptions < ArgumentError; end class InvalidOptions < ArgumentError; end
class RedisServerIsTooOld < Exception; end class RedisServerIsTooOld < Exception; end

View File

@@ -18,6 +18,7 @@ module Redistat
end end
def create(options = {}) def create(options = {})
#TODO clean/remove all ref-less connections
ref = options.delete(:ref) || :default ref = options.delete(:ref) || :default
options.reverse_merge!(default_options) options.reverse_merge!(default_options)
conn = (connections[connection_id(options)] ||= connection(options)) conn = (connections[connection_id(options)] ||= connection(options))
@@ -26,15 +27,11 @@ module Redistat
end end
def connections def connections
threaded[:connections] ||= {} @connections ||= {}
end end
def references def references
threaded[:references] ||= {} @references ||= {}
end
def threaded
Thread.current[:redistat] ||= {}
end end
private private

View File

@@ -85,11 +85,4 @@ module Redistat
end end
end end
module DateHelper
def to_redistat(depth = nil)
Redistat::Date.new(self, depth)
end
alias :to_rs :to_redistat
end
end end

View File

@@ -0,0 +1,8 @@
module Redistat
module DateHelper
def to_redistat(depth = nil)
Redistat::Date.new(self, depth)
end
alias :to_rs :to_redistat
end
end

View File

@@ -31,7 +31,10 @@ module Redistat
end end
def default_options def default_options
{ :depth => :hour, :store_event => false, :connection_ref => nil } { :depth => :hour,
:store_event => false,
:connection_ref => nil,
:enable_grouping => true }
end end
def new? def new?
@@ -72,7 +75,7 @@ module Redistat
def save def save
return false if !self.new? return false if !self.new?
Summary.update_all(@key, @stats, depth_limit, @connection_ref) Summary.update_all(@key, @stats, depth_limit, @connection_ref, @options[:enable_grouping])
if @options[:store_event] if @options[:store_event]
@id = self.next_id @id = self.next_id
db.hmset("#{self.scope}#{KEY_EVENT}#{@id}", db.hmset("#{self.scope}#{KEY_EVENT}#{@id}",

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

@@ -39,10 +39,20 @@ module Redistat
@label.hash @label.hash
end end
def label_groups
@label.groups
end
def label=(input) def label=(input)
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options) @label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options)
end end
def groups
@groups ||= label_groups.map do |label_name|
self.class.new(@scope, label_name, self.date, @options)
end
end
def to_s(depth = nil) def to_s(depth = nil)
depth ||= @options[:depth] depth ||= @options[:depth]
key = self.prefix key = self.prefix

View File

@@ -5,6 +5,10 @@ module Redistat
attr_reader :raw attr_reader :raw
attr_reader :connection_ref attr_reader :connection_ref
def self.create(name, options = {})
self.new(name, options).save
end
def initialize(str, options = {}) def initialize(str, options = {})
@options = options @options = options
@raw = str.to_s @raw = str.to_s
@@ -31,8 +35,18 @@ module Redistat
@saved ||= false @saved ||= false
end end
def self.create(name, options = {}) def groups
self.new(name, options).save return @groups if @groups
@groups = []
parent = ""
@raw.split(GROUP_SEPARATOR).each do |part|
if !part.blank?
group = ((parent.blank?) ? "" : "#{parent}/") + part
@groups << group
parent = group
end
end
@groups.reverse!
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,5 +1,5 @@
module Redistat module Redistat
class Result < ::ActiveSupport::HashWithIndifferentAccess class Result < HashWithIndifferentAccess
attr_accessor :from attr_accessor :from
attr_accessor :till attr_accessor :till

View File

@@ -2,23 +2,57 @@ module Redistat
class Summary class Summary
include Database include Database
def self.update_all(key, stats = {}, depth_limit = nil, connection_ref = nil) def self.update_all(key, stats = {}, depth_limit = nil, connection_ref = nil, enable_grouping = nil)
stats ||= {} stats ||= {}
depth_limit ||= key.depth
return nil if stats.size == 0 return nil if stats.size == 0
depth_limit ||= key.depth
enable_grouping = true if enable_grouping.nil?
if enable_grouping
stats = inject_group_summaries(stats)
key.groups.each { |k|
update_key(k, stats, depth_limit, connection_ref)
}
else
update_key(key, stats, depth_limit, connection_ref)
end
end
private
def self.update_key(key, stats, depth_limit, connection_ref)
Date::DEPTHS.each do |depth| Date::DEPTHS.each do |depth|
update(key, stats, depth, connection_ref) update(key, stats, depth, connection_ref)
break if depth == depth_limit break if depth == depth_limit
end end
end end
private
def self.update(key, stats, depth, connection_ref = nil) def self.update(key, stats, depth, connection_ref = nil)
stats.each do |field, value| stats.each do |field, value|
db(connection_ref).hincrby key.to_s(depth), field, value db(connection_ref).hincrby key.to_s(depth), field, value
end end
end end
def self.inject_group_summaries!(stats)
stats.each do |key, value|
parts = key.to_s.split(GROUP_SEPARATOR)
parts.pop
if parts.size > 0
sum_parts = []
parts.each do |part|
sum_parts << part
sum_key = sum_parts.join(GROUP_SEPARATOR)
(stats.has_key?(sum_key)) ? stats[sum_key] += value : stats[sum_key] = value
end
end
end
stats
end
def self.inject_group_summaries(stats)
inject_group_summaries!(stats.clone)
end
end end
end end

View File

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

View File

@@ -19,11 +19,12 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.add_runtime_dependency 'activesupport', '>= 2.3.0' s.add_runtime_dependency 'activesupport', '>= 2.3.6'
s.add_runtime_dependency 'json', '>= 1.4.0' s.add_runtime_dependency 'json', '>= 1.4.0'
s.add_runtime_dependency 'redis', '>= 2.1.0' s.add_runtime_dependency 'redis', '>= 2.1.0'
s.add_runtime_dependency 'time_ext', '>= 0.2.8' s.add_runtime_dependency 'time_ext', '>= 0.2.8'
s.add_development_dependency 'rspec', '>= 2.1.0' s.add_development_dependency 'rspec', '>= 2.1.0'
s.add_development_dependency 'rcov', '>= 0.9.9'
s.add_development_dependency 'yard', '>= 0.6.3' s.add_development_dependency 'yard', '>= 0.6.3'
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

@@ -14,6 +14,7 @@ describe Redistat::Key do
@key.scope.should == @scope @key.scope.should == @scope
@key.label.should == @label @key.label.should == @label
@key.label_hash.should == @label_hash @key.label_hash.should == @label_hash
@key.label_groups.should == @key.instance_variable_get("@label").groups
@key.date.should be_instance_of(Redistat::Date) @key.date.should be_instance_of(Redistat::Date)
@key.date.to_time.to_s.should == @date.to_s @key.date.to_time.to_s.should == @date.to_s
end end
@@ -60,4 +61,16 @@ describe Redistat::Key do
@key.label_hash == @label_hash @key.label_hash == @label_hash
end end
it "should create a group of keys from label group" do
label = 'message/public/offensive'
result = [ "message/public/offensive",
"message/public",
"message" ]
key = Redistat::Key.new(@scope, label, @date, {:depth => :hour})
key.label_groups.should == result
key.groups.map { |k| k.label }.should == result
end
end end

View File

@@ -25,4 +25,24 @@ describe Redistat::Label do
db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == name db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == name
end end
it "should separate label names into groups" do
name = "message/public/offensive"
label = Redistat::Label.new(name)
label.name.should == name
label.groups.should == [ "message/public/offensive",
"message/public",
"message" ]
name = "/message/public/"
label = Redistat::Label.new(name)
label.name.should == name
label.groups.should == [ "message/public",
"message" ]
name = "message"
label = Redistat::Label.new(name)
label.name.should == name
label.groups.should == [ "message" ]
end
end end

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

View File

@@ -8,5 +8,5 @@ require 'rspec'
require 'rspec/autorun' require 'rspec/autorun'
# use the test Redistat instance # use the test Redistat instance
Redistat.connect(:port => 8379, :db => 15) Redistat.connect(:port => 8379, :db => 15, :thread_safe => true)
Redistat.redis.flushdb Redistat.redis.flushdb

View File

@@ -46,4 +46,78 @@ describe Redistat::Summary do
end end
end end
it "should inject stats key grouping summaries" do
hash = { "count/hello" => 3, "count/world" => 7,
"death/bomb" => 4, "death/unicorn" => 3,
:"od/sugar" => 7, :"od/meth" => 8 }
res = Redistat::Summary.send(:inject_group_summaries, hash)
res.should == { "count" => 10, "count/hello" => 3, "count/world" => 7,
"death" => 7, "death/bomb" => 4, "death/unicorn" => 3,
"od" => 15, :"od/sugar" => 7, :"od/meth" => 8 }
end
it "should properly store key group summaries" do
stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
Redistat::Summary.update_all(@key, stats, :hour)
summary = db.hgetall(@key.to_s(:hour))
summary.should have(4).items
summary["views"].should == "3"
summary["visitors"].should == "6"
summary["visitors/eu"].should == "2"
summary["visitors/us"].should == "4"
end
it "should not store key group summaries when option is disabled" do
stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
Redistat::Summary.update_all(@key, stats, :hour, nil, false)
summary = db.hgetall(@key.to_s(:hour))
summary.should have(3).items
summary["views"].should == "3"
summary["visitors/eu"].should == "2"
summary["visitors/us"].should == "4"
end
it "should store label-based grouping enabled stats" do
stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
label = "views/about_us"
key = Redistat::Key.new(@scope, label, @date)
Redistat::Summary.update_all(key, stats, :hour)
key.groups[0].label.should == "views/about_us"
key.groups[1].label.should == "views"
child1 = key.groups[0]
parent = key.groups[1]
label = "views/contact"
key = Redistat::Key.new(@scope, label, @date)
Redistat::Summary.update_all(key, stats, :hour)
key.groups[0].label.should == "views/contact"
key.groups[1].label.should == "views"
child2 = key.groups[0]
summary = db.hgetall(child1.to_s(:hour))
summary["views"].should == "3"
summary["visitors/eu"].should == "2"
summary["visitors/us"].should == "4"
summary = db.hgetall(child2.to_s(:hour))
summary["views"].should == "3"
summary["visitors/eu"].should == "2"
summary["visitors/us"].should == "4"
summary = db.hgetall(parent.to_s(:hour))
summary["views"].should == "6"
summary["visitors/eu"].should == "4"
summary["visitors/us"].should == "8"
end
end end

View File

@@ -0,0 +1,39 @@
require "spec_helper"
describe "Thread-Safety" do
include Redistat::Database
before(:each) do
db.flushdb
end
#TODO should have more comprehensive thread-safe tests
it "should incr in multiple threads" do
threads = []
50.times do
threads << Thread.new {
db.incr("spec:incr")
}
end
threads.each { |t| t.join }
db.get("spec:incr").should == "50"
end
it "should store event in multiple threads" do
class ThreadSafetySpec
include Redistat::Model
end
threads = []
50.times do
threads << Thread.new {
ThreadSafetySpec.store("spec:threadsafe", {:count => 1, :rand => rand(5)})
}
end
threads.each { |t| t.join }
result = ThreadSafetySpec.fetch("spec:threadsafe", 5.hours.ago, 5.hours.from_now)
result.total[:count].should == 50
result.total[:rand].should <= 250
end
end