26 Commits

Author SHA1 Message Date
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
21 changed files with 240 additions and 69 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

@@ -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

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

@@ -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

@@ -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.0"
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

@@ -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

@@ -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