mirror of
https://github.com/jimeh/redistat.git
synced 2026-02-19 13:26:39 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecbd15ef1e | |||
| 2b2461dd9f | |||
| 3e177c9ae4 | |||
| d4cd5402bc | |||
| d560a7deff | |||
| 3df6666704 | |||
| 80fd63059b | |||
| d0b7f097a8 | |||
| 875d16b01c | |||
| 00791d36d8 | |||
| 94b589c5e6 | |||
| cdf52869d6 | |||
| 8b711d4d9c | |||
| e4aaedfe58 | |||
| ea820d44f4 | |||
| acedf071d1 | |||
| 108b6ab02e | |||
| 9920c0dc2f | |||
| a72ad31c51 | |||
| 008228660e | |||
| b8ddcdf71a | |||
| 94fcd5b4ae | |||
| e2a551d01c | |||
| 43fc8bc2dd | |||
| dcca3556ea | |||
| d5f79b82a9 | |||
| 0938781cd1 | |||
| 82119fcf69 | |||
| 746d0fea8f | |||
| 331bf81e3a | |||
| f2c026c1eb | |||
| 57517983f6 | |||
| 53aee885bd | |||
| 8001a98a26 | |||
| 34331a655e | |||
| 91ad5b2d3c | |||
| 9fd5ae8545 | |||
| 14b7f4768e | |||
| 57274ffb21 | |||
| 8d063c98e5 | |||
| d39d5d8dde | |||
| 3a00353f83 | |||
| 49fc2afcfd | |||
| cfbe58a509 | |||
| 629f46ed89 | |||
| 9faa0db7b8 | |||
| 834614ab79 | |||
| 47a1b0737c | |||
| 5d3c181641 | |||
| e3f23433d9 | |||
| 482253f517 | |||
| 66b9f4e949 | |||
| 7a28d1210f | |||
| d74dc41110 | |||
| ac338bb4f0 | |||
| 325a264411 | |||
| dc3816f691 | |||
| 57d8fddd23 | |||
| c5e9c02c84 | |||
| e0eac61a59 | |||
| 33e9477552 | |||
| 7e8e1dacc7 | |||
| 06cd30a20c | |||
| f481554fc9 | |||
| a9b6f2c99a | |||
| 8710f4a51f | |||
| 96e9b0a736 | |||
| 102fb41a6b | |||
| b0a44a6abc | |||
| f8dfb034af | |||
| 15904e8a94 | |||
| fe221c3f31 | |||
| 7b1feda061 | |||
| 968ef47ac5 | |||
| e3c4a5da9a | |||
| b215c6d56c | |||
| 8d5c73a539 | |||
| 0d5170bc26 | |||
| 4692780d1e | |||
| f8ec626762 | |||
| ec54385192 | |||
| 4808a97d19 | |||
| 1dce2780e0 | |||
|
|
cab7ed5633 | ||
| 861d040187 | |||
| 3267ee1eb9 | |||
| 6309e4b217 | |||
| 776ee8ac97 | |||
| 3b346e88e0 | |||
| c3fe861b10 | |||
| bc5034b6bb | |||
| 66510fe344 | |||
| 745473862f | |||
| a5c8fc6fbf | |||
| 115b223d7c | |||
| 0597b587fd | |||
| 89932759ef | |||
| 55e0687837 | |||
|
|
93360dbeb9 | ||
|
|
6a66605e0b | ||
|
|
d9ce0daade | ||
| f6ec2e97b2 | |||
| 67dc9433c7 | |||
| f0fcd2110d | |||
| 24112e4705 | |||
| b9752ff92f | |||
| 14a093d79b | |||
| 84a05363dd | |||
| 690d1d9407 | |||
| 2aedd4eee3 | |||
| f906cf068e | |||
| cbb9050c80 | |||
| 58a2fb560c | |||
| 6bae8ce2bc | |||
| 18e6125c6a | |||
| dc162e0c89 | |||
| 5338676a5f | |||
| 02fe41082a | |||
| 65e7745419 | |||
| 490356ee96 | |||
| a6c4600aa5 | |||
| 85ba61b2cc | |||
| 8f6a4a6820 | |||
| 81ee2ec0b6 | |||
| 20280f2c5d | |||
| bf29696c46 | |||
| 92375b229a | |||
| e362c93d9a | |||
| 0f5b7449b0 | |||
| ea732b4734 | |||
| 62c3492c93 | |||
| c5f52455cc | |||
| 1226f8b89a |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -16,11 +16,12 @@ tmtags
|
||||
## PROJECT::GENERAL
|
||||
coverage
|
||||
rdoc
|
||||
pkg
|
||||
pkg/*
|
||||
*.gem
|
||||
.bundle
|
||||
Gemfile.lock
|
||||
|
||||
## PROJECT::SPECIFIC
|
||||
.bundle/*
|
||||
.yardoc/*
|
||||
spec/db/*
|
||||
doc
|
||||
redistat.gemspec
|
||||
doc/*
|
||||
|
||||
12
Gemfile
12
Gemfile
@@ -1,12 +1,4 @@
|
||||
source 'http://rubygems.org/'
|
||||
|
||||
gem 'activesupport', '>= 2.3.0'
|
||||
gem 'json', '>= 1.0.0'
|
||||
gem 'redis', '>= 2.0.0'
|
||||
gem 'time_ext', '>= 0.2.6'
|
||||
|
||||
group :development do
|
||||
gem 'rspec', '>= 2.0.1'
|
||||
gem 'yard', '>= 0.6.1'
|
||||
gem 'i18n'
|
||||
end
|
||||
# Specify your gem's dependencies in redistat.gemspec
|
||||
gemspec
|
||||
|
||||
31
Gemfile.lock
31
Gemfile.lock
@@ -1,31 +0,0 @@
|
||||
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.6)
|
||||
activesupport (>= 2.3.0)
|
||||
yard (0.6.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (>= 2.3.0)
|
||||
i18n
|
||||
json (>= 1.0.0)
|
||||
redis (>= 2.0.0)
|
||||
rspec (>= 2.0.1)
|
||||
time_ext (>= 0.2.6)
|
||||
yard (>= 0.6.1)
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
70
README.md
70
README.md
@@ -2,11 +2,73 @@
|
||||
|
||||
A Redis-backed statistics storage and querying library written in Ruby.
|
||||
|
||||
## Early Beta
|
||||
Redistat was originally created to replace a small hacked together statistics collection solution which was MySQL-based. When I started I had a short list of requirements:
|
||||
|
||||
Currently this is an early beta release. Readme and documentation is forthcoming.
|
||||
* Store and increment/decrement integer values (counters, etc)
|
||||
* Up to the second statistics available at all times
|
||||
* Screamingly fast
|
||||
|
||||
For now, please check `spec/model_spec.rb` and `spec/model_helper.rb` to get started with how to use Redistat.
|
||||
Redis fits perfectly with all of these requirements. It has atomic operations like increment, and it's lightning fast, meaning if the data is structured well, the initial stats reporting call will store data in a format that's instantly retrievable just as fast.
|
||||
|
||||
## Installation
|
||||
|
||||
gem install redistat
|
||||
|
||||
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
|
||||
|
||||
The simplest way to use Redistat is through the model wrapper.
|
||||
|
||||
class VisitorStats
|
||||
include Redistat::Model
|
||||
end
|
||||
|
||||
Before any of you Rails-purists start complaining about the model name being plural, I want to point out that it makes sense with Redistat, cause a model doesn't exactly return a specific row or object. But I'm getting ahead of myself.
|
||||
|
||||
To store statistics we essentially tell Redistat that an event has occurred with a label of X, and statistics of Y. So let's say we want to store a page view event on the `/about` page on a site:
|
||||
|
||||
VisitorStats.store('/about', {:views => 1})
|
||||
|
||||
In the above case "`/about`" is the label under which the stats are grouped, and the statistics associated with the event is simply a normal Ruby hash, except all values need to be integers, or Redis' increment calls won't work.
|
||||
|
||||
To later retrieve statistics, we use the `fetch` method:
|
||||
|
||||
stats = VisitorStats.fetch('/about', 2.hour.ago, Time.now)
|
||||
# stats => [{:views => 1}]
|
||||
# stats.total => {:views => 1}
|
||||
|
||||
The fetch method requires 3 arguments, a label, a start time, and an end time. Fetch returns a `Redistat::Collection` object, which is normal Ruby Array with a couple of added methods, like total shown above.
|
||||
|
||||
For more detailed usage, please check spec files, and source code till I have time to write up a complete readme.
|
||||
|
||||
|
||||
## Some Technical Details
|
||||
|
||||
To give a brief look into how Redistat works internally to store statistics, I'm going to use the examples above. The store method accepts a Ruby Hash with statistics to store. Redistat stores all statistics as hashes in Redis. It stores summaries of the stats for the specific time when it happened and all it's parent time groups (second, minute, hour, day, month, year). The default depth Redistat goes to is hour, unless the `depth` option is passed to `store` or `fetch`.
|
||||
|
||||
In short, the above call to `store` creates the following keys in Redis:
|
||||
|
||||
VisitorStats//about:2010
|
||||
VisitorStats//about:201011
|
||||
VisitorStats//about:20101124
|
||||
VisitorStats//about:2010112401
|
||||
|
||||
Each of these keys in Redis are a hash, containing the sums of each statistic point reported for the time frame the key represents. In this case there's two slashes, cause the label we used was “`/about`”, and the scope (class name when used through the model wrapper) and the label are separated with a slash.
|
||||
|
||||
When retrieving statistics for a given date range, Redistat figures out how to do the least number of calls to Redis to fetch all relevant data. For example, if you want the sum of stats from the 4th till the last of November, the full month of November would first be fetched, then the first 3 days of November would be fetched and removed from the full month stats.
|
||||
|
||||
|
||||
## Todo
|
||||
|
||||
* Proper/complete readme.
|
||||
* Documentation.
|
||||
* Anything else that becomes apparent after real-world use.
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
[Global Personals](http://globalpersonals.co.uk/) deserves a thank you. Currently the primary user of Redistat, they've allowed me to spend some company time to further develop the project.
|
||||
|
||||
|
||||
## Note on Patches/Pull Requests
|
||||
@@ -22,7 +84,7 @@ For now, please check `spec/model_spec.rb` and `spec/model_helper.rb` to get sta
|
||||
|
||||
## License and Copyright
|
||||
|
||||
Copyright (c) 2010 Jim Myhrberg.
|
||||
Copyright (c) 2011 Jim Myhrberg.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
50
Rakefile
50
Rakefile
@@ -1,29 +1,11 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
|
||||
begin
|
||||
require 'jeweler'
|
||||
Jeweler::Tasks.new do |gem|
|
||||
gem.name = 'redistat'
|
||||
gem.summary = 'A Redis-backed statistics storage and querying library written in Ruby.'
|
||||
gem.description = 'A Redis-backed statistics storage and querying library written in Ruby.'
|
||||
gem.email = 'contact@jimeh.me'
|
||||
gem.homepage = 'http://github.com/jimeh/redistat'
|
||||
gem.authors = ['Jim Myhrberg']
|
||||
gem.add_dependency 'activesupport', '>= 2.3.0'
|
||||
gem.add_dependency 'json', '>= 1.0.0'
|
||||
gem.add_dependency 'redis', '>= 2.0.0'
|
||||
gem.add_dependency 'time_ext', '>= 0.2.6'
|
||||
gem.add_development_dependency 'rspec', '>= 2.0.1'
|
||||
gem.add_development_dependency 'yard', '>= 0.6.1'
|
||||
end
|
||||
Jeweler::GemcutterTasks.new
|
||||
rescue LoadError
|
||||
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
||||
end
|
||||
require 'bundler'
|
||||
Bundler::GemHelper.install_tasks
|
||||
|
||||
|
||||
#
|
||||
# Rspec
|
||||
#
|
||||
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new(:spec) do |spec|
|
||||
spec.pattern = 'spec/**/*_spec.rb'
|
||||
@@ -32,14 +14,16 @@ end
|
||||
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
||||
spec.pattern = 'spec/**/*_spec.rb'
|
||||
spec.rcov = true
|
||||
spec.rcov_opts = ['--exclude', 'spec']
|
||||
end
|
||||
|
||||
task :spec => :check_dependencies
|
||||
|
||||
task :default => [:start, :spec, :stop]
|
||||
|
||||
|
||||
#
|
||||
# Start/stop Redis test server
|
||||
#
|
||||
|
||||
REDIS_DIR = File.expand_path(File.join("..", "spec"), __FILE__)
|
||||
REDIS_CNF = File.join(REDIS_DIR, "redis-test.conf")
|
||||
REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
|
||||
@@ -60,7 +44,10 @@ task :stop do
|
||||
end
|
||||
|
||||
|
||||
# YARD Documentation
|
||||
#
|
||||
# Yard
|
||||
#
|
||||
|
||||
begin
|
||||
require 'yard'
|
||||
YARD::Rake::YardocTask.new
|
||||
@@ -69,3 +56,14 @@ rescue LoadError
|
||||
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Misc.
|
||||
#
|
||||
|
||||
desc "Start an irb console with Redistat pre-loaded."
|
||||
task :console do
|
||||
exec "irb -r spec/spec_helper"
|
||||
end
|
||||
task :c => :console
|
||||
|
||||
120
lib/redistat.rb
120
lib/redistat.rb
@@ -1,104 +1,70 @@
|
||||
|
||||
require 'rubygems'
|
||||
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 'redis'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'time_ext'
|
||||
require 'json'
|
||||
require 'digest/sha1'
|
||||
|
||||
require 'redistat/collection'
|
||||
# Active Support 2.x or 3.x
|
||||
require 'active_support'
|
||||
if !{}.respond_to?(:with_indifferent_access)
|
||||
require 'active_support/core_ext/hash/indifferent_access'
|
||||
require 'active_support/core_ext/hash/reverse_merge'
|
||||
end
|
||||
|
||||
require 'time_ext'
|
||||
require 'redis'
|
||||
require 'json'
|
||||
|
||||
require 'redistat/options'
|
||||
require 'redistat/connection'
|
||||
require 'redistat/database'
|
||||
require 'redistat/collection'
|
||||
require 'redistat/date'
|
||||
require 'redistat/date_helper'
|
||||
require 'redistat/event'
|
||||
require 'redistat/finder'
|
||||
require 'redistat/finder/date_set'
|
||||
require 'redistat/key'
|
||||
require 'redistat/label'
|
||||
require 'redistat/model'
|
||||
require 'redistat/result'
|
||||
require 'redistat/scope'
|
||||
require 'redistat/summary'
|
||||
require 'redistat/version'
|
||||
|
||||
require 'redistat/core_ext/date'
|
||||
require 'redistat/core_ext/time'
|
||||
require 'redistat/core_ext/fixnum'
|
||||
require 'redistat/core_ext'
|
||||
|
||||
module Redistat
|
||||
|
||||
KEY_NEXT_ID = ".next_id"
|
||||
KEY_EVENT = ".event:"
|
||||
KEY_LEBELS = "Redistat.lables:"
|
||||
KEY_LABELS = "Redistat.labels:" # used for reverse label hash lookup
|
||||
KEY_EVENT_IDS = ".event_ids"
|
||||
LABEL_INDEX = ".label_index:"
|
||||
GROUP_SEPARATOR = "/"
|
||||
|
||||
class InvalidOptions < ArgumentError; end
|
||||
|
||||
# Provides access to the Redis database. This is shared accross all models and instances.
|
||||
def redis
|
||||
threaded[:redis] ||= connection(*options)
|
||||
end
|
||||
|
||||
def redis=(connection)
|
||||
threaded[:redis] = connection
|
||||
end
|
||||
|
||||
def threaded
|
||||
Thread.current[:redistat] ||= {}
|
||||
end
|
||||
|
||||
# Connect to a redis database.
|
||||
#
|
||||
# @param options [Hash] options to create a message with.
|
||||
# @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
|
||||
# @option options [#to_s] :port (6379) Port number.
|
||||
# @option options [#to_s] :db (0) Database number.
|
||||
# @option options [#to_s] :timeout (0) Database timeout in seconds.
|
||||
# @example Connect to a database in port 6380.
|
||||
# Redistat.connect(:port => 6380)
|
||||
def connect(*options)
|
||||
self.redis = nil
|
||||
@options = options
|
||||
end
|
||||
|
||||
# Return a connection to Redis.
|
||||
#
|
||||
# This is a wapper around Redis.new(options)
|
||||
def connection(*options)
|
||||
Redis.new(*options)
|
||||
end
|
||||
|
||||
def options
|
||||
@options = [] unless defined? @options
|
||||
@options
|
||||
end
|
||||
|
||||
# Clear the database.
|
||||
def flush
|
||||
redis.flushdb
|
||||
end
|
||||
class RedisServerIsTooOld < Exception; end
|
||||
|
||||
module_function :connect, :connection, :flush, :redis, :redis=, :options, :threaded
|
||||
|
||||
class << self
|
||||
|
||||
def connection(ref = nil)
|
||||
Connection.get(ref)
|
||||
end
|
||||
alias :redis :connection
|
||||
|
||||
def connection=(connection)
|
||||
Connection.add(connection)
|
||||
end
|
||||
alias :redis= :connection=
|
||||
|
||||
def connect(options)
|
||||
Connection.create(options)
|
||||
end
|
||||
|
||||
def flush
|
||||
puts "WARNING: Redistat.flush is deprecated. Use Redistat.redis.flushdb instead."
|
||||
connection.flushdb
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,5 +12,9 @@ module Redistat
|
||||
@depth = options[:depth] ||= nil
|
||||
end
|
||||
|
||||
def total
|
||||
@total ||= {}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
64
lib/redistat/connection.rb
Normal file
64
lib/redistat/connection.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
module Redistat
|
||||
module Connection
|
||||
|
||||
REQUIRED_SERVER_VERSION = "1.3.10"
|
||||
|
||||
class << self
|
||||
|
||||
def get(ref = nil)
|
||||
ref ||= :default
|
||||
connections[references[ref]] || create
|
||||
end
|
||||
|
||||
def add(conn, ref = nil)
|
||||
ref ||= :default
|
||||
check_redis_version(conn)
|
||||
references[ref] = conn.client.id
|
||||
connections[conn.client.id] = conn
|
||||
end
|
||||
|
||||
def create(options = {})
|
||||
#TODO clean/remove all ref-less connections
|
||||
ref = options.delete(:ref) || :default
|
||||
options.reverse_merge!(default_options)
|
||||
conn = (connections[connection_id(options)] ||= connection(options))
|
||||
references[ref] = conn.client.id
|
||||
conn
|
||||
end
|
||||
|
||||
def connections
|
||||
@connections ||= {}
|
||||
end
|
||||
|
||||
def references
|
||||
@references ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_redis_version(conn)
|
||||
raise RedisServerIsTooOld if conn.info["redis_version"] < REQUIRED_SERVER_VERSION
|
||||
conn
|
||||
end
|
||||
|
||||
def connection(options)
|
||||
check_redis_version(Redis.new(options))
|
||||
end
|
||||
|
||||
def connection_id(options = {})
|
||||
options.reverse_merge!(default_options)
|
||||
"redis://#{options[:host]}:#{options[:port]}/#{options[:db]}"
|
||||
end
|
||||
|
||||
def default_options
|
||||
{
|
||||
:host => '127.0.0.1',
|
||||
:port => 6379,
|
||||
:db => 0,
|
||||
:timeout => 5
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
5
lib/redistat/core_ext.rb
Normal file
5
lib/redistat/core_ext.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
require 'redistat/core_ext/bignum'
|
||||
require 'redistat/core_ext/date'
|
||||
require 'redistat/core_ext/fixnum'
|
||||
require 'redistat/core_ext/hash'
|
||||
require 'redistat/core_ext/time'
|
||||
8
lib/redistat/core_ext/bignum.rb
Executable file
8
lib/redistat/core_ext/bignum.rb
Executable file
@@ -0,0 +1,8 @@
|
||||
class Bignum
|
||||
include Redistat::DateHelper
|
||||
|
||||
def to_time
|
||||
Time.at(self)
|
||||
end
|
||||
|
||||
end
|
||||
23
lib/redistat/core_ext/hash.rb
Normal file
23
lib/redistat/core_ext/hash.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
class Hash
|
||||
|
||||
def merge_and_incr(hash)
|
||||
self.clone.merge_and_incr!(hash)
|
||||
end
|
||||
|
||||
def merge_and_incr!(hash)
|
||||
raise ArgumentError unless hash.is_a?(Hash)
|
||||
hash.each do |key, value|
|
||||
self[key] = value unless self.set_or_incr(key, value)
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def set_or_incr(key, value)
|
||||
return false unless value.is_a?(Numeric)
|
||||
self[key] = 0 unless self.has_key?(key)
|
||||
return false unless self[key].is_a?(Numeric)
|
||||
self[key] += value
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
@@ -3,8 +3,9 @@ module Redistat
|
||||
def self.included(base)
|
||||
base.extend(Database)
|
||||
end
|
||||
def db
|
||||
Redistat.redis
|
||||
def db(ref = nil)
|
||||
ref ||= @options[:connection_ref] if !@options.nil?
|
||||
Redistat.connection(ref)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,6 +22,8 @@ module Redistat
|
||||
from_string(input)
|
||||
elsif input.is_a?(::Fixnum)
|
||||
from_integer(input)
|
||||
elsif input.is_a?(::Bignum)
|
||||
from_integer(input)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,11 +85,4 @@ module Redistat
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module DateHelper
|
||||
def to_redistat(depth = nil)
|
||||
Redistat::Date.new(self, depth)
|
||||
end
|
||||
alias :to_rs :to_redistat
|
||||
end
|
||||
end
|
||||
|
||||
8
lib/redistat/date_helper.rb
Normal file
8
lib/redistat/date_helper.rb
Normal 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
|
||||
@@ -1,25 +1,29 @@
|
||||
module Redistat
|
||||
class Event
|
||||
include Database
|
||||
include Options
|
||||
|
||||
attr_reader :id
|
||||
attr_reader :key
|
||||
|
||||
attr_accessor :stats
|
||||
attr_accessor :meta
|
||||
attr_accessor :options
|
||||
|
||||
def initialize(scope, label = nil, date = nil, stats = {}, options = {}, meta = {}, is_new = true)
|
||||
@options = default_options.merge(options)
|
||||
def default_options
|
||||
{ :depth => :hour,
|
||||
:store_event => false,
|
||||
:connection_ref => nil,
|
||||
:enable_grouping => true,
|
||||
:label_indexing => true }
|
||||
end
|
||||
|
||||
def initialize(scope, label = nil, date = nil, stats = {}, opts = {}, meta = {}, is_new = true)
|
||||
parse_options(opts)
|
||||
@key = Key.new(scope, label, date, @options)
|
||||
@stats = stats ||= {}
|
||||
@meta = meta ||= {}
|
||||
@new = is_new
|
||||
end
|
||||
|
||||
def default_options
|
||||
{ :depth => :hour, :store_event => false }
|
||||
end
|
||||
|
||||
def new?
|
||||
@new
|
||||
@@ -59,7 +63,7 @@ module Redistat
|
||||
|
||||
def save
|
||||
return false if !self.new?
|
||||
Summary.update_all(@key, @stats, depth_limit)
|
||||
Summary.update_all(@key, @stats, depth_limit, @options)
|
||||
if @options[:store_event]
|
||||
@id = self.next_id
|
||||
db.hmset("#{self.scope}#{KEY_EVENT}#{@id}",
|
||||
@@ -87,7 +91,7 @@ module Redistat
|
||||
event = db.hgetall "#{scope}#{KEY_EVENT}#{id}"
|
||||
return nil if event.size == 0
|
||||
self.new( event["scope"], event["label"], event["date"], JSON.parse(event["stats"]),
|
||||
JSON.parse(event["meta"]), JSON.parse(event["options"]), false )
|
||||
JSON.parse(event["options"]), JSON.parse(event["meta"]), false )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,94 +1,10 @@
|
||||
require 'redistat/finder/date_set'
|
||||
|
||||
module Redistat
|
||||
class Finder
|
||||
include Database
|
||||
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
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
|
||||
|
||||
def find(*args)
|
||||
new.find(*args)
|
||||
end
|
||||
@@ -122,44 +38,199 @@ module Redistat
|
||||
def interval(unit)
|
||||
new.interval(unit)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def scope(scope)
|
||||
@options[:scope] = scope
|
||||
attr_reader :options
|
||||
|
||||
def initialize(opts = {})
|
||||
set_options(opts)
|
||||
end
|
||||
|
||||
def 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 parent
|
||||
@parent ||= self.class.new(options.merge(:label => options[:label].parent)) unless options[:label].nil?
|
||||
end
|
||||
|
||||
def children
|
||||
build_key.children.map { |key|
|
||||
self.class.new(options.merge(:label => key.label.to_s))
|
||||
}
|
||||
end
|
||||
|
||||
def connection_ref(ref = nil)
|
||||
return options[:connection_ref] if ref.nil?
|
||||
reset! if options[:connection_ref] != ref
|
||||
options[:connection_ref] = ref
|
||||
self
|
||||
end
|
||||
|
||||
def label(label)
|
||||
@options[:label] = label
|
||||
def scope(input = nil)
|
||||
return options[:scope] if input.nil?
|
||||
reset! if !options[:scope].nil? && options[:scope].to_s != input.to_s
|
||||
options[:scope] = Scope.new(input)
|
||||
self
|
||||
end
|
||||
|
||||
def dates(from, till)
|
||||
from(from).till(till)
|
||||
def label(input = nil)
|
||||
return options[:label] if input.nil?
|
||||
reset! if options.has_key?(:label) && options[:label].to_s != input.to_s
|
||||
options[:label] = (!input.nil?) ? Label.new(input) : nil
|
||||
self
|
||||
end
|
||||
|
||||
def dates(start, finish)
|
||||
from(start).till(finish)
|
||||
end
|
||||
alias :date :dates
|
||||
|
||||
def from(date)
|
||||
@options[:from] = date
|
||||
def from(date = nil)
|
||||
return options[:from] if date.nil?
|
||||
reset! if options[:from] != date
|
||||
options[:from] = date
|
||||
self
|
||||
end
|
||||
|
||||
def till(date)
|
||||
@options[:till] = date
|
||||
def till(date = nil)
|
||||
return options[:till] if date.nil?
|
||||
reset! if options[:till] != date
|
||||
options[:till] = date
|
||||
self
|
||||
end
|
||||
alias :until :till
|
||||
|
||||
def depth(unit)
|
||||
@options[:depth] = unit
|
||||
def depth(unit = nil)
|
||||
return options[:depth] if unit.nil?
|
||||
reset! if options[:depth] != unit
|
||||
options[:depth] = unit
|
||||
self
|
||||
end
|
||||
|
||||
def interval(unit)
|
||||
@options[:interval] = unit
|
||||
def interval(unit = nil)
|
||||
return options[:interval] if unit.nil?
|
||||
reset! if options[:interval] != unit
|
||||
options[:interval] = unit
|
||||
self
|
||||
end
|
||||
|
||||
def find(opts = {})
|
||||
set_options(opts)
|
||||
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
|
||||
self.options.merge!(opts)
|
||||
end
|
||||
|
||||
def find_by_interval
|
||||
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
|
||||
raise InvalidOptions.new if !valid_options?
|
||||
key = build_key
|
||||
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
|
||||
@parent = 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
|
||||
@@ -9,6 +9,10 @@ module Redistat
|
||||
end
|
||||
|
||||
def find_date_sets(start_date, end_date, depth = nil, interval = false)
|
||||
if depth.nil? && interval.is_a?(Symbol)
|
||||
depth = interval
|
||||
interval = true
|
||||
end
|
||||
start_date = start_date.to_time if start_date.is_a?(::Date)
|
||||
end_date = end_date.to_time if end_date.is_a?(::Date)
|
||||
if !interval
|
||||
@@ -42,7 +46,7 @@ module Redistat
|
||||
return find_start_year_for(start_date, end_date, lowest_depth) if unit == :year
|
||||
index = Date::DEPTHS.index(unit)
|
||||
nunit = Date::DEPTHS[(index > 0) ? index-1 : index]
|
||||
if start_date < start_date.round(nunit) || start_date.next(nunit).beginning_of(nunit) > end_date.beginning_of(nunit)
|
||||
if start_date < start_date.beginning_of_closest(nunit) || start_date.next(nunit).beginning_of(nunit) > end_date.beginning_of(nunit)
|
||||
add = []
|
||||
start_date.beginning_of_each(unit, :include_start => lowest_depth).until(start_date.end_of(nunit)) do |t|
|
||||
add << t.to_rs.to_s(unit) if t < end_date.beginning_of(unit)
|
||||
@@ -59,7 +63,7 @@ module Redistat
|
||||
index = Date::DEPTHS.index(unit)
|
||||
nunit = Date::DEPTHS[(index > 0) ? index-1 : index]
|
||||
has_nunit = end_date.prev(nunit).beginning_of(nunit) >= start_date.beginning_of(nunit)
|
||||
nearest_nunit = end_date.round(nunit)
|
||||
nearest_nunit = end_date.beginning_of_closest(nunit)
|
||||
if end_date >= nearest_nunit && has_nunit
|
||||
add = []
|
||||
end_date.beginning_of(nunit).beginning_of_each(unit, :include_start => true, :include_end => lowest_depth).until(end_date) do |t|
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
module Redistat
|
||||
class Key
|
||||
include Database
|
||||
include Options
|
||||
|
||||
attr_accessor :scope
|
||||
attr_accessor :date
|
||||
attr_accessor :options
|
||||
def default_options
|
||||
{ :depth => :hour }
|
||||
end
|
||||
|
||||
def initialize(scope, label_name = nil, time_stamp = nil, options = {})
|
||||
@options = default_options.merge(options || {})
|
||||
@scope = scope
|
||||
def initialize(scope, label_name = nil, time_stamp = nil, opts = {})
|
||||
parse_options(opts)
|
||||
self.scope = scope
|
||||
self.label = label_name if !label_name.nil?
|
||||
self.date = time_stamp ||= Time.now
|
||||
end
|
||||
|
||||
def default_options
|
||||
{ :depth => :hour, :hashed_label => false }
|
||||
end
|
||||
|
||||
def prefix
|
||||
key = "#{@scope}"
|
||||
key << "/#{label}" if !label.nil?
|
||||
key << "/#{label.name}" if !label.nil?
|
||||
key << ":"
|
||||
key
|
||||
end
|
||||
@@ -26,21 +24,52 @@ module Redistat
|
||||
def date=(input)
|
||||
@date = (input.instance_of?(Redistat::Date)) ? input : Date.new(input) # Redistat::Date, not ::Date
|
||||
end
|
||||
attr_reader :date
|
||||
|
||||
def depth
|
||||
@options[:depth]
|
||||
options[:depth]
|
||||
end
|
||||
|
||||
def label
|
||||
@label.name
|
||||
# def scope
|
||||
# @scope.to_s
|
||||
# end
|
||||
|
||||
def scope=(input)
|
||||
@scope = (input.instance_of?(Redistat::Scope)) ? input : Scope.new(input)
|
||||
end
|
||||
attr_reader :scope
|
||||
|
||||
def label=(input)
|
||||
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options)
|
||||
end
|
||||
attr_reader :label
|
||||
|
||||
def label_hash
|
||||
@label.hash
|
||||
end
|
||||
|
||||
def label=(input)
|
||||
@label = (input.instance_of?(Redistat::Label)) ? input : Label.create(input, @options)
|
||||
def parent
|
||||
@parent ||= self.class.new(self.scope, @label.parent, self.date, @options) unless @label.parent.nil?
|
||||
end
|
||||
|
||||
def children
|
||||
db.smembers("#{scope}#{LABEL_INDEX}#{@label}").map { |member|
|
||||
child_label = [@label, member].reject { |i| i.nil? }
|
||||
self.class.new(self.scope, child_label.join(GROUP_SEPARATOR), self.date, @options)
|
||||
}
|
||||
end
|
||||
|
||||
def update_index
|
||||
@label.groups.each do |label|
|
||||
parent = (label.parent || "")
|
||||
db.sadd("#{scope}#{LABEL_INDEX}#{parent}", label.me)
|
||||
end
|
||||
end
|
||||
|
||||
def groups
|
||||
@groups ||= @label.groups.map do |label|
|
||||
self.class.new(@scope, label, self.date, @options)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s(depth = nil)
|
||||
|
||||
@@ -1,33 +1,68 @@
|
||||
module Redistat
|
||||
class Label
|
||||
include Database
|
||||
include Options
|
||||
|
||||
attr_reader :raw
|
||||
def default_options
|
||||
{ :hashed_label => false }
|
||||
end
|
||||
|
||||
def initialize(str, options = {})
|
||||
@options = options
|
||||
def self.create(name, opts = {})
|
||||
self.new(name, opts).save
|
||||
end
|
||||
|
||||
def self.join(*args)
|
||||
args = args.map {|i| i.to_s}
|
||||
self.new(args.reject {|i| i.blank? }.join(GROUP_SEPARATOR))
|
||||
end
|
||||
|
||||
def initialize(str, opts = {})
|
||||
parse_options(opts)
|
||||
@raw = str.to_s
|
||||
end
|
||||
|
||||
def to_s
|
||||
@raw
|
||||
end
|
||||
|
||||
def name
|
||||
@options[:hashed_label] ? hash : @raw
|
||||
@options[:hashed_label] ? hash : self.to_s
|
||||
end
|
||||
|
||||
def hash
|
||||
@hash ||= Digest::SHA1.hexdigest(@raw)
|
||||
@hash ||= Digest::SHA1.hexdigest(self.to_s)
|
||||
end
|
||||
|
||||
def save
|
||||
@saved = (db.set("#{KEY_LEBELS}#{hash}", @raw) == "OK") if @options[:hashed_label]
|
||||
@saved = db.hset(KEY_LABELS, hash, self.to_s) if @options[:hashed_label]
|
||||
self
|
||||
end
|
||||
|
||||
def saved?
|
||||
return true unless @options[:hashed_label]
|
||||
@saved ||= false
|
||||
end
|
||||
|
||||
def self.create(name, options = {})
|
||||
self.new(name, options).save
|
||||
def parent
|
||||
@parent ||= groups[1] if groups.size > 1
|
||||
end
|
||||
|
||||
def me
|
||||
self.to_s.split(GROUP_SEPARATOR).last
|
||||
end
|
||||
|
||||
def groups
|
||||
return @groups unless @groups.nil?
|
||||
@groups = []
|
||||
parent = ""
|
||||
self.to_s.split(GROUP_SEPARATOR).each do |part|
|
||||
if !part.blank?
|
||||
group = ((parent.blank?) ? "" : "#{parent}#{GROUP_SEPARATOR}") + part
|
||||
@groups << Label.new(group)
|
||||
parent = group
|
||||
end
|
||||
end
|
||||
@groups.reverse!
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,58 +1,69 @@
|
||||
module Redistat
|
||||
module Model
|
||||
include Database
|
||||
include Options
|
||||
|
||||
def self.included(base)
|
||||
base.extend(self)
|
||||
end
|
||||
|
||||
def store(label, stats = {}, date = nil, meta = {}, opts = {})
|
||||
Event.new(name, label, date, stats, options.merge(opts), meta).save
|
||||
|
||||
#
|
||||
# statistics store/fetch methods
|
||||
#
|
||||
|
||||
def store(label, stats = {}, date = nil, opts = {}, meta = {})
|
||||
Event.new(self.name, label, date, stats, options.merge(opts), meta).save
|
||||
end
|
||||
alias :event :store
|
||||
|
||||
|
||||
def fetch(label, from, till, opts = {})
|
||||
Finder.find({
|
||||
:scope => name,
|
||||
:label => label,
|
||||
:from => from,
|
||||
:till => till
|
||||
}.merge(options.merge(opts)))
|
||||
find(label, from, till, opts).all
|
||||
end
|
||||
alias :find :fetch
|
||||
|
||||
def hashed_label(boolean = nil)
|
||||
if !boolean.nil?
|
||||
options[:hashed_label] = boolean
|
||||
else
|
||||
options[:hashed_label] || nil
|
||||
end
|
||||
alias :lookup :fetch
|
||||
|
||||
def find(label, from, till, opts = {})
|
||||
Finder.new( { :scope => self.name,
|
||||
:label => label,
|
||||
:from => from,
|
||||
:till => till }.merge(options.merge(opts)) )
|
||||
end
|
||||
|
||||
def depth(depth = nil)
|
||||
if !depth.nil?
|
||||
options[:depth] = depth
|
||||
else
|
||||
options[:depth] || nil
|
||||
end
|
||||
def find_event(event_id)
|
||||
Event.find(self.name, event_id)
|
||||
end
|
||||
|
||||
def store_event(boolean = nil)
|
||||
if !boolean.nil?
|
||||
options[:store_event] = boolean
|
||||
else
|
||||
options[:store_event] || nil
|
||||
end
|
||||
|
||||
#
|
||||
# options methods
|
||||
#
|
||||
|
||||
option_accessor :depth
|
||||
option_accessor :class_name
|
||||
option_accessor :store_event
|
||||
option_accessor :hashed_label
|
||||
option_accessor :label_indexing
|
||||
|
||||
alias :scope :class_name
|
||||
|
||||
def connect_to(opts = {})
|
||||
Connection.create(opts.merge(:ref => name))
|
||||
options[:connection_ref] = name
|
||||
end
|
||||
|
||||
def options
|
||||
@options ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
#
|
||||
# resource access methods
|
||||
#
|
||||
|
||||
def connection
|
||||
db(options[:connection_ref])
|
||||
end
|
||||
alias :redis :connection
|
||||
|
||||
def name
|
||||
@name ||= self.to_s
|
||||
options[:class_name] || (@name ||= self.to_s)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
41
lib/redistat/options.rb
Normal file
41
lib/redistat/options.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
module Redistat
|
||||
module Options
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def option_accessor(*opts)
|
||||
opts.each do |option|
|
||||
define_method(option) do |*args|
|
||||
if !args.first.nil?
|
||||
options[option.to_sym] = args.first
|
||||
else
|
||||
options[option.to_sym] || nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_options(opts)
|
||||
opts ||= {}
|
||||
@raw_options = opts
|
||||
@options = default_options.merge(opts.reject { |k,v| v.nil? })
|
||||
end
|
||||
|
||||
def default_options
|
||||
{}
|
||||
end
|
||||
|
||||
def options
|
||||
@options ||= {}
|
||||
end
|
||||
|
||||
def raw_options
|
||||
@raw_options ||= {}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
module Redistat
|
||||
class Result < ::ActiveSupport::HashWithIndifferentAccess
|
||||
class Result < HashWithIndifferentAccess
|
||||
|
||||
attr_accessor :from
|
||||
attr_accessor :till
|
||||
@@ -12,12 +12,5 @@ module Redistat
|
||||
@till = options[:till] ||= nil
|
||||
end
|
||||
|
||||
|
||||
def set_or_incr(key, value)
|
||||
self[key] = 0 if !self.has_key?(key)
|
||||
self[key] += value
|
||||
self
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,23 +2,66 @@ module Redistat
|
||||
class Summary
|
||||
include Database
|
||||
|
||||
def self.update_all(key, stats = {}, depth_limit = nil)
|
||||
def self.default_options
|
||||
{ :enable_grouping => true,
|
||||
:label_indexing => true,
|
||||
:connection_ref => nil }
|
||||
end
|
||||
|
||||
def self.update_all(key, stats = {}, depth_limit = nil, opts = {})
|
||||
stats ||= {}
|
||||
depth_limit ||= key.depth
|
||||
return nil if stats.size == 0
|
||||
Date::DEPTHS.each do |depth|
|
||||
update(key, stats, depth)
|
||||
break if depth == depth_limit
|
||||
|
||||
options = default_options.merge((opts || {}).reject { |k,v| v.nil? })
|
||||
|
||||
depth_limit ||= key.depth
|
||||
|
||||
if options[:enable_grouping]
|
||||
stats = inject_group_summaries(stats)
|
||||
key.groups.each do |k|
|
||||
update_key(k, stats, depth_limit, options[:connection_ref])
|
||||
k.update_index if options[:label_indexing]
|
||||
end
|
||||
else
|
||||
update_key(key, stats, depth_limit, options[:connection_ref])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.update(key, stats, depth)
|
||||
stats.each do |field, value|
|
||||
db.hincrby key.to_s(depth), field, value
|
||||
def self.update_key(key, stats, depth_limit, connection_ref)
|
||||
Date::DEPTHS.each do |depth|
|
||||
update(key, stats, depth, connection_ref)
|
||||
break if depth == depth_limit
|
||||
end
|
||||
end
|
||||
|
||||
def self.update(key, stats, depth, connection_ref = nil)
|
||||
stats.each do |field, value|
|
||||
db(connection_ref).hincrby key.to_s(depth), field, value
|
||||
end
|
||||
end
|
||||
|
||||
def self.inject_group_summaries!(stats)
|
||||
summaries = {}
|
||||
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)
|
||||
(summaries.has_key?(sum_key)) ? summaries[sum_key] += value : summaries[sum_key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
stats.merge_and_incr!(summaries)
|
||||
end
|
||||
|
||||
def self.inject_group_summaries(stats)
|
||||
inject_group_summaries!(stats.clone)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
3
lib/redistat/version.rb
Normal file
3
lib/redistat/version.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module Redistat
|
||||
VERSION = "0.2.4"
|
||||
end
|
||||
30
redistat.gemspec
Normal file
30
redistat.gemspec
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
$:.push File.expand_path("../lib", __FILE__)
|
||||
require "redistat/version"
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "redistat"
|
||||
s.version = Redistat::VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.authors = ["Jim Myhrberg"]
|
||||
s.email = ["contact@jimeh.me"]
|
||||
s.homepage = "http://github.com/jimeh/redistat"
|
||||
s.summary = %q{A Redis-backed statistics storage and querying library written in Ruby.}
|
||||
s.description = %q{A Redis-backed statistics storage and querying library written in Ruby.}
|
||||
|
||||
s.rubyforge_project = "redistat"
|
||||
|
||||
s.files = `git ls-files`.split("\n")
|
||||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
||||
s.require_paths = ["lib"]
|
||||
|
||||
s.add_runtime_dependency 'activesupport', '>= 2.3.6'
|
||||
s.add_runtime_dependency 'json', '>= 1.4.0'
|
||||
s.add_runtime_dependency 'redis', '>= 2.1.0'
|
||||
s.add_runtime_dependency 'time_ext', '>= 0.2.9'
|
||||
|
||||
s.add_development_dependency 'rspec', '>= 2.1.0'
|
||||
s.add_development_dependency 'rcov', '>= 0.9.9'
|
||||
s.add_development_dependency 'yard', '>= 0.6.3'
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Redistat do
|
||||
include Redistat::Database
|
||||
|
||||
before(:each) do
|
||||
db.flushdb
|
||||
end
|
||||
|
||||
it "should have a valid Redis client instance" do
|
||||
db.should_not be_nil
|
||||
end
|
||||
|
||||
it "should be connected to the testing server" do
|
||||
db.client.port.should == 8379
|
||||
db.client.host.should == "127.0.0.1"
|
||||
end
|
||||
|
||||
it "should be able to set and get data" do
|
||||
db.set("hello", "world")
|
||||
db.get("hello").should == "world"
|
||||
db.del("hello").should be_true
|
||||
end
|
||||
|
||||
it "should be able to store hashes to Redis" do
|
||||
db.hset("key", "field", "1")
|
||||
db.hget("key", "field").should == "1"
|
||||
db.hincrby("key", "field", 1)
|
||||
db.hget("key", "field").should == "2"
|
||||
db.hincrby("key", "field", -1)
|
||||
db.hget("key", "field").should == "1"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -2,7 +2,7 @@ require "spec_helper"
|
||||
|
||||
describe Redistat::Collection do
|
||||
|
||||
it "should should initialize properly" do
|
||||
it "should initialize properly" do
|
||||
options = {:from => "from", :till => "till", :depth => "depth"}
|
||||
result = Redistat::Collection.new(options)
|
||||
result.from.should == options[:from]
|
||||
@@ -10,4 +10,11 @@ describe Redistat::Collection do
|
||||
result.depth.should == options[:depth]
|
||||
end
|
||||
|
||||
it "should have a total property" do
|
||||
col = Redistat::Collection.new()
|
||||
col.total.should == {}
|
||||
col.total = {:foo => "bar"}
|
||||
col.total.should == {:foo => "bar"}
|
||||
end
|
||||
|
||||
end
|
||||
61
spec/connection_spec.rb
Normal file
61
spec/connection_spec.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
require "spec_helper"
|
||||
include Redistat
|
||||
|
||||
describe Redistat::Connection do
|
||||
|
||||
it "should have a valid Redis client instance" do
|
||||
Redistat.redis.should_not be_nil
|
||||
end
|
||||
|
||||
it "should have initialized custom testing connection" do
|
||||
redis = Redistat.redis
|
||||
redis.client.host.should == '127.0.0.1'
|
||||
redis.client.port.should == 8379
|
||||
redis.client.db.should == 15
|
||||
end
|
||||
|
||||
it "should be able to set and get data" do
|
||||
redis = Redistat.redis
|
||||
redis.set("hello", "world")
|
||||
redis.get("hello").should == "world"
|
||||
redis.del("hello").should be_true
|
||||
end
|
||||
|
||||
it "should be able to store hashes to Redis" do
|
||||
redis = Redistat.redis
|
||||
redis.hset("hash", "field", "1")
|
||||
redis.hget("hash", "field").should == "1"
|
||||
redis.hincrby("hash", "field", 1)
|
||||
redis.hget("hash", "field").should == "2"
|
||||
redis.hincrby("hash", "field", -1)
|
||||
redis.hget("hash", "field").should == "1"
|
||||
redis.del("hash")
|
||||
end
|
||||
|
||||
it "should be accessible from Redistat module" do
|
||||
Redistat.redis.should == Connection.get
|
||||
Redistat.redis.should == Redistat.connection
|
||||
end
|
||||
|
||||
it "should handle multiple connections with refs" do
|
||||
Redistat.redis.client.db.should == 15
|
||||
Redistat.connect(:port => 8379, :db => 14, :ref => "Custom")
|
||||
Redistat.redis.client.db.should == 15
|
||||
Redistat.redis("Custom").client.db.should == 14
|
||||
end
|
||||
|
||||
it "should be able to overwrite default and custom refs" do
|
||||
Redistat.redis.client.db.should == 15
|
||||
Redistat.connect(:port => 8379, :db => 14)
|
||||
Redistat.redis.client.db.should == 14
|
||||
|
||||
Redistat.redis("Custom").client.db.should == 14
|
||||
Redistat.connect(:port => 8379, :db => 15, :ref => "Custom")
|
||||
Redistat.redis("Custom").client.db.should == 15
|
||||
|
||||
# Reset the default connection to the testing server or all hell
|
||||
# might brake loose from the rest of the specs
|
||||
Redistat.connect(:port => 8379, :db => 15)
|
||||
end
|
||||
|
||||
end
|
||||
30
spec/core_ext/hash_spec.rb
Normal file
30
spec/core_ext/hash_spec.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Hash do
|
||||
|
||||
it "should #set_or_incr values" do
|
||||
hash = {:count => 1}
|
||||
hash.set_or_incr(:sum, 3).should be_true
|
||||
hash.should == {:count => 1, :sum => 3}
|
||||
hash.set_or_incr(:count, 4).should be_true
|
||||
hash.should == {:count => 5, :sum => 3}
|
||||
hash.set_or_incr(:count, 'test').should be_false
|
||||
hash.set_or_incr(:view, 'test').should be_false
|
||||
hash.should == {:count => 5, :sum => 3}
|
||||
hash[:view] = 'test'
|
||||
hash.set_or_incr(:view, 3).should be_false
|
||||
end
|
||||
|
||||
it "should #merge_and_incr hashes" do
|
||||
hash = { :count => 1, :city => 'hell', :sum => 3, :name => 'john' }
|
||||
|
||||
new_hash = { :count => 3, :city => 'slum', :views => 2 }
|
||||
hash.clone.merge_and_incr(new_hash).should == { :count => 4, :city => 'slum', :views => 2,
|
||||
:sum => 3, :name => 'john' }
|
||||
|
||||
new_hash = { :count => 'six', :city => 'slum', :views => 2, :time => 'late' }
|
||||
hash.clone.merge_and_incr(new_hash).should == { :count => 'six', :city => 'slum', :views => 2,
|
||||
:sum => 3, :name => 'john', :time => 'late' }
|
||||
end
|
||||
|
||||
end
|
||||
10
spec/database_spec.rb
Normal file
10
spec/database_spec.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Redistat::Database do
|
||||
include Redistat::Database
|
||||
|
||||
it "should make #db method available when included" do
|
||||
db.should == Redistat.redis
|
||||
end
|
||||
|
||||
end
|
||||
@@ -8,8 +8,8 @@ describe Redistat::Event do
|
||||
@scope = "PageViews"
|
||||
@label = "about_us"
|
||||
@label_hash = Digest::SHA1.hexdigest(@label)
|
||||
@stats = {:views => 1}
|
||||
@meta = {:user_id => 239}
|
||||
@stats = {'views' => 1}
|
||||
@meta = {'user_id' => 239}
|
||||
@options = {:depth => :hour}
|
||||
@date = Time.now
|
||||
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options, @meta)
|
||||
@@ -17,10 +17,10 @@ describe Redistat::Event do
|
||||
|
||||
it "should initialize properly" do
|
||||
@event.id.should be_nil
|
||||
@event.scope.should == @scope
|
||||
@event.label.should == @label
|
||||
@event.scope.to_s.should == @scope
|
||||
@event.label.to_s.should == @label
|
||||
@event.label_hash.should == @label_hash
|
||||
@event.date.to_time.should == @date
|
||||
@event.date.to_time.to_s.should == @date.to_s
|
||||
@event.stats.should == @stats
|
||||
@event.meta.should == @meta
|
||||
@event.options.should == @event.default_options.merge(@options)
|
||||
@@ -28,17 +28,17 @@ describe Redistat::Event do
|
||||
|
||||
it "should allow changing attributes" do
|
||||
# date
|
||||
@event.date.to_time.should == @date
|
||||
@event.date.to_time.to_s.should == @date.to_s
|
||||
@date = Time.now
|
||||
@event.date = @date
|
||||
@event.date.to_time.should == @date
|
||||
@event.date.to_time.to_s.should == @date.to_s
|
||||
# label
|
||||
@event.label.should == @label
|
||||
@event.label.to_s.should == @label
|
||||
@event.label_hash.should == @label_hash
|
||||
@label = "contact_us"
|
||||
@label_hash = Digest::SHA1.hexdigest(@label)
|
||||
@event.label = @label
|
||||
@event.label.should == @label
|
||||
@event.label.to_s.should == @label
|
||||
@event.label_hash.should == @label_hash
|
||||
end
|
||||
|
||||
@@ -63,9 +63,11 @@ describe Redistat::Event do
|
||||
it "should find event by id" do
|
||||
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options.merge({:store_event => true}), @meta).save
|
||||
fetched = Redistat::Event.find(@scope, @event.id)
|
||||
@event.scope.should == fetched.scope
|
||||
@event.label.should == fetched.label
|
||||
@event.scope.to_s.should == fetched.scope.to_s
|
||||
@event.label.to_s.should == fetched.label.to_s
|
||||
@event.date.to_s.should == fetched.date.to_s
|
||||
@event.stats.should == fetched.stats
|
||||
@event.meta.should == fetched.meta
|
||||
end
|
||||
|
||||
it "should store summarized statistics" do
|
||||
|
||||
@@ -28,11 +28,13 @@ describe Redistat::Finder::DateSet do
|
||||
result = Redistat::Finder::DateSet.new.find_date_sets(t_start, t_end, :hour, true)
|
||||
result[0][:add].should == ["2010082818", "2010082819", "2010082820", "2010082821", "2010082822"]
|
||||
result[0][:rem].should == []
|
||||
result.should == Redistat::Finder::DateSet.new(t_start, t_end, nil, :hour)
|
||||
|
||||
t_end = t_start + 4.days
|
||||
result = Redistat::Finder::DateSet.new.find_date_sets(t_start, t_end, :day, true)
|
||||
result[0][:add].should == ["20100828", "20100829", "20100830", "20100831", "20100901"]
|
||||
result[0][:rem].should == []
|
||||
result.should == Redistat::Finder::DateSet.new(t_start, t_end, nil, :day)
|
||||
end
|
||||
|
||||
it "should find start keys properly" do
|
||||
|
||||
@@ -9,38 +9,52 @@ describe Redistat::Finder do
|
||||
@label = "about_us"
|
||||
@date = Time.now
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :day})
|
||||
@stats = {"views" => 3, "visitors" => 2}
|
||||
@stats = {"views" => 3, "visitors" => 2}
|
||||
@two_hours_ago = 2.hours.ago
|
||||
@one_hour_ago = 1.hour.ago
|
||||
end
|
||||
|
||||
it "should initialize properly" do
|
||||
two_hours_ago = 2.hours.ago
|
||||
one_hour_ago = 1.hour.ago
|
||||
options = {:scope => "PageViews", :label => "Label", :from => two_hours_ago, :till => one_hour_ago, :depth => :hour, :interval => :hour}
|
||||
options = {:scope => "PageViews", :label => "Label", :from => @two_hours_ago, :till => @one_hour_ago, :depth => :hour, :interval => :hour}
|
||||
|
||||
finder = Redistat::Finder.new(options)
|
||||
finder.options.should == options
|
||||
|
||||
finder = Redistat::Finder.dates(two_hours_ago, one_hour_ago).scope("PageViews").label("Label").depth(:hour).interval(:hour)
|
||||
finder.options.should == options
|
||||
|
||||
finder = Redistat::Finder.scope("PageViews").label("Label").from(two_hours_ago).till(one_hour_ago).depth(:hour).interval(:hour)
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.new
|
||||
finder.send(:set_options, options)
|
||||
finder.options[:scope].should be_a(Redistat::Scope)
|
||||
finder.options[:scope].to_s.should == options[:scope]
|
||||
finder.options[:label].should be_a(Redistat::Label)
|
||||
finder.options[:label].to_s.should == options[:label]
|
||||
finder.options.should == options.merge(:scope => finder.options[:scope], :label => finder.options[:label])
|
||||
|
||||
finder = Redistat::Finder.label("Label").from(two_hours_ago).till(one_hour_ago).depth(:hour).interval(:hour).scope("PageViews")
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.scope("hello")
|
||||
finder.options[:scope].to_s.should == "hello"
|
||||
finder.scope.to_s.should == "hello"
|
||||
|
||||
finder = Redistat::Finder.from(two_hours_ago).till(one_hour_ago).depth(:hour).interval(:hour).scope("PageViews").label("Label")
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.label("hello")
|
||||
finder.options[:label].to_s.should == "hello"
|
||||
finder.label.to_s.should == "hello"
|
||||
|
||||
finder = Redistat::Finder.till(one_hour_ago).depth(:hour).interval(:hour).scope("PageViews").label("Label").from(two_hours_ago)
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.dates(@two_hours_ago, @one_hour_ago)
|
||||
finder.options[:from].should == @two_hours_ago
|
||||
finder.options[:till].should == @one_hour_ago
|
||||
|
||||
finder = Redistat::Finder.depth(:hour).interval(:hour).scope("PageViews").label("Label").from(two_hours_ago).till(one_hour_ago)
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.from(@two_hours_ago)
|
||||
finder.options[:from].should == @two_hours_ago
|
||||
finder.from.should == @two_hours_ago
|
||||
|
||||
finder = Redistat::Finder.interval(:hour).scope("PageViews").label("Label").from(two_hours_ago).till(one_hour_ago).depth(:hour)
|
||||
finder.options.should == options
|
||||
finder = Redistat::Finder.till(@one_hour_ago)
|
||||
finder.options[:till].should == @one_hour_ago
|
||||
finder.till.should == @one_hour_ago
|
||||
|
||||
finder = Redistat::Finder.depth(:hour)
|
||||
finder.options[:depth].should == :hour
|
||||
finder.depth.should == :hour
|
||||
|
||||
finder = Redistat::Finder.interval(true)
|
||||
finder.options[:interval].should be_true
|
||||
finder.interval.should be_true
|
||||
finder = Redistat::Finder.interval(false)
|
||||
finder.options[:interval].should be_false
|
||||
finder.interval.should be_false
|
||||
end
|
||||
|
||||
it "should fetch stats properly" do
|
||||
@@ -85,6 +99,96 @@ describe Redistat::Finder do
|
||||
lambda { Redistat::Finder.find(:from => 3.hours.ago) }.should raise_error(Redistat::InvalidOptions)
|
||||
end
|
||||
|
||||
describe "Grouping" do
|
||||
before(:each) do
|
||||
@options = {:scope => "PageViews", :label => "message/public", :from => @two_hours_ago, :till => @one_hour_ago, :depth => :hour, :interval => :hour}
|
||||
@finder = Redistat::Finder.new(@options)
|
||||
end
|
||||
|
||||
it "should return parent finder" do
|
||||
@finder.instance_variable_get("@parent").should be_nil
|
||||
@finder.parent.should be_a(Redistat::Finder)
|
||||
@finder.instance_variable_get("@parent").should_not be_nil
|
||||
@finder.parent.options[:label].to_s.should == 'message'
|
||||
@finder.label('message')
|
||||
@finder.instance_variable_get("@parent").should be_nil
|
||||
@finder.parent.should_not be_nil
|
||||
@finder.parent.options[:label].should be_nil
|
||||
@finder.parent.parent.should be_nil
|
||||
end
|
||||
|
||||
it "should find children" do
|
||||
Redistat::Key.new("PageViews", "message/public/die").update_index
|
||||
Redistat::Key.new("PageViews", "message/public/live").update_index
|
||||
Redistat::Key.new("PageViews", "message/public/fester").update_index
|
||||
members = db.smembers("#{@scope}#{Redistat::LABEL_INDEX}message/public") # checking 'message/public'
|
||||
@finder.children.first.should be_a(Redistat::Finder)
|
||||
subs = @finder.children.map { |f| f.options[:label].me }
|
||||
subs.should have(3).items
|
||||
subs.should include('die')
|
||||
subs.should include('live')
|
||||
subs.should include('fester')
|
||||
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 # "Lazy-Loading"
|
||||
|
||||
|
||||
# helper methods
|
||||
|
||||
@@ -93,7 +197,7 @@ describe Redistat::Finder do
|
||||
Redistat::Summary.update(key, @stats, :hour)
|
||||
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 13:53"))
|
||||
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)
|
||||
key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02")))
|
||||
Redistat::Summary.update(key, @stats, :hour)
|
||||
@@ -101,12 +205,3 @@ describe Redistat::Finder do
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Redistat::Key do
|
||||
include Redistat::Database
|
||||
|
||||
before(:each) do
|
||||
db.flushdb
|
||||
@scope = "PageViews"
|
||||
@label = "about_us"
|
||||
@label_hash = Digest::SHA1.hexdigest(@label)
|
||||
@@ -11,9 +13,10 @@ describe Redistat::Key do
|
||||
end
|
||||
|
||||
it "should initialize properly" do
|
||||
@key.scope.should == @scope
|
||||
@key.label.should == @label
|
||||
@key.scope.to_s.should == @scope
|
||||
@key.label.to_s.should == @label
|
||||
@key.label_hash.should == @label_hash
|
||||
@key.groups.map { |k| k.instance_variable_get("@label") }.should == @key.instance_variable_get("@label").groups
|
||||
@key.date.should be_instance_of(Redistat::Date)
|
||||
@key.date.to_time.to_s.should == @date.to_s
|
||||
end
|
||||
@@ -25,6 +28,8 @@ describe Redistat::Key do
|
||||
@key.to_s(props.last).should == "#{@scope}/#{@label}:#{@key.date.to_s(props.last)}"
|
||||
props.pop
|
||||
end
|
||||
key = Redistat::Key.new(@scope, nil, @date, {:depth => :hour})
|
||||
key.to_s.should == "#{@scope}:#{key.date.to_s(:hour)}"
|
||||
end
|
||||
|
||||
it "should abide to hashed_label option" do
|
||||
@@ -41,23 +46,84 @@ describe Redistat::Key do
|
||||
|
||||
it "should allow changing attributes" do
|
||||
# scope
|
||||
@key.scope.should == @scope
|
||||
@key.scope.to_s.should == @scope
|
||||
@scope = "VisitorCount"
|
||||
@key.scope = @scope
|
||||
@key.scope.should == @scope
|
||||
@key.scope.to_s.should == @scope
|
||||
# date
|
||||
@key.date.to_time.should == @date
|
||||
@key.date.to_time.to_s.should == @date.to_s
|
||||
@date = Time.now
|
||||
@key.date = @date
|
||||
@key.date.to_time.should == @date
|
||||
@key.date.to_time.to_s.should == @date.to_s
|
||||
# label
|
||||
@key.label.should == @label
|
||||
@key.label.to_s.should == @label
|
||||
@key.label_hash == @label_hash
|
||||
@label = "contact_us"
|
||||
@label_hash = Digest::SHA1.hexdigest(@label)
|
||||
@key.label = @label
|
||||
@key.label.should == @label
|
||||
@key.label.to_s.should == @label
|
||||
@key.label_hash == @label_hash
|
||||
end
|
||||
|
||||
describe "Grouping" do
|
||||
before(:each) do
|
||||
@label = "message/public/offensive"
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour})
|
||||
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.groups.map { |k| k.label.to_s }.should == result
|
||||
end
|
||||
|
||||
it "should know it's parent" do
|
||||
@key.parent.should be_a(Redistat::Key)
|
||||
@key.parent.label.to_s.should == 'message/public'
|
||||
Redistat::Key.new(@scope, 'hello', @date).parent.should be_nil
|
||||
end
|
||||
|
||||
it "should update label index and return children" do
|
||||
db.smembers("#{@scope}#{Redistat::LABEL_INDEX}#{@key.label.parent}").should == []
|
||||
@key.children.should have(0).items
|
||||
|
||||
@key.update_index # indexing 'message/publish/offensive'
|
||||
Redistat::Key.new("PageViews", "message/public/die").update_index # indexing 'message/publish/die'
|
||||
Redistat::Key.new("PageViews", "message/public/live").update_index # indexing 'message/publish/live'
|
||||
|
||||
members = db.smembers("#{@scope}#{Redistat::LABEL_INDEX}#{@key.label.parent}") # checking 'message/public'
|
||||
members.should have(3).item
|
||||
members.should include('offensive')
|
||||
members.should include('live')
|
||||
members.should include('die')
|
||||
|
||||
key = @key.parent
|
||||
key.children.first.should be_a(Redistat::Key)
|
||||
key.children.should have(3).item
|
||||
key.children.map { |k| k.label.me }.should == members
|
||||
|
||||
members = db.smembers("#{@scope}#{Redistat::LABEL_INDEX}#{key.label.parent}") # checking 'message'
|
||||
members.should have(1).item
|
||||
members.should include('public')
|
||||
|
||||
key = key.parent
|
||||
key.children.should have(1).item
|
||||
key.children.map { |k| k.label.me }.should == members
|
||||
|
||||
members = db.smembers("#{@scope}#{Redistat::LABEL_INDEX}") # checking ''
|
||||
members.should have(1).item
|
||||
members.should include('message')
|
||||
|
||||
key.parent.should be_nil
|
||||
key = Redistat::Key.new("PageViews")
|
||||
key.children.should have(1).item
|
||||
key.children.map { |k| k.label.me }.should include('message')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -17,12 +17,55 @@ describe Redistat::Label do
|
||||
it "should store a label hash lookup key" do
|
||||
label = Redistat::Label.new(@name, {:hashed_label => true}).save
|
||||
label.saved?.should be_true
|
||||
db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == @name
|
||||
db.hget(Redistat::KEY_LABELS, label.hash).should == @name
|
||||
|
||||
name = "contact_us"
|
||||
label = Redistat::Label.create(name, {:hashed_label => true})
|
||||
label.saved?.should be_true
|
||||
db.get("#{Redistat::KEY_LEBELS}#{label.hash}").should == name
|
||||
db.hget(Redistat::KEY_LABELS, label.hash).should == name
|
||||
end
|
||||
|
||||
it "should join labels" do
|
||||
include Redistat
|
||||
label = Label.join('email', 'message', 'public')
|
||||
label.should be_a(Label)
|
||||
label.to_s.should == 'email/message/public'
|
||||
label = Label.join(Label.new('email'), Label.new('message'), Label.new('public'))
|
||||
label.should be_a(Label)
|
||||
label.to_s.should == 'email/message/public'
|
||||
label = Label.join('email', '', 'message', nil, 'public')
|
||||
label.should be_a(Label)
|
||||
label.to_s.should == 'email/message/public'
|
||||
end
|
||||
|
||||
describe "Grouping" do
|
||||
before(:each) do
|
||||
@name = "message/public/offensive"
|
||||
@label = Redistat::Label.new(@name)
|
||||
end
|
||||
|
||||
it "should know it's parent label group" do
|
||||
@label.parent.to_s.should == 'message/public'
|
||||
Redistat::Label.new('hello').parent.should be_nil
|
||||
end
|
||||
|
||||
it "should separate label names into groups" do
|
||||
@label.name.should == @name
|
||||
@label.groups.map { |l| l.to_s }.should == [ "message/public/offensive",
|
||||
"message/public",
|
||||
"message" ]
|
||||
|
||||
@name = "/message/public/"
|
||||
@label = Redistat::Label.new(@name)
|
||||
@label.name.should == @name
|
||||
@label.groups.map { |l| l.to_s }.should == [ "message/public",
|
||||
"message" ]
|
||||
|
||||
@name = "message"
|
||||
@label = Redistat::Label.new(@name)
|
||||
@label.name.should == @name
|
||||
@label.groups.map { |l| l.to_s }.should == [ "message" ]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
require "redistat"
|
||||
|
||||
class ModelHelper
|
||||
class ModelHelper1
|
||||
include Redistat::Model
|
||||
|
||||
|
||||
@@ -13,4 +13,18 @@ class ModelHelper2
|
||||
store_event true
|
||||
hashed_label true
|
||||
|
||||
end
|
||||
|
||||
class ModelHelper3
|
||||
include Redistat::Model
|
||||
|
||||
connect_to :port => 8379, :db => 14
|
||||
|
||||
end
|
||||
|
||||
class ModelHelper4
|
||||
include Redistat::Model
|
||||
|
||||
class_name "FancyHelper"
|
||||
|
||||
end
|
||||
@@ -5,62 +5,155 @@ describe Redistat::Model do
|
||||
include Redistat::Database
|
||||
|
||||
before(:each) do
|
||||
db.flushdb
|
||||
@time = Time.utc(2010, 8, 28, 12, 0, 0)
|
||||
ModelHelper1.redis.flushdb
|
||||
ModelHelper2.redis.flushdb
|
||||
ModelHelper3.redis.flushdb
|
||||
ModelHelper4.redis.flushdb
|
||||
end
|
||||
|
||||
it "should should name itself correctly" do
|
||||
ModelHelper.send(:name).should == "ModelHelper"
|
||||
ModelHelper1.send(:name).should == "ModelHelper1"
|
||||
ModelHelper2.send(:name).should == "ModelHelper2"
|
||||
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].to_s.should == 'ModelHelper1'
|
||||
finder.options[:label].to_s.should == 'label'
|
||||
finder.options[:from].should == two_hours_ago
|
||||
finder.options[:till].should == one_hour_ago
|
||||
end
|
||||
|
||||
it "should #find_event" do
|
||||
Redistat::Event.should_receive(:find).with('ModelHelper1', 1)
|
||||
ModelHelper1.find_event(1)
|
||||
end
|
||||
|
||||
it "should listen to model-defined options" do
|
||||
ModelHelper2.depth.should == :day
|
||||
ModelHelper2.store_event.should == true
|
||||
ModelHelper2.hashed_label.should == true
|
||||
ModelHelper2.class_name.should be_nil
|
||||
|
||||
ModelHelper.depth.should == nil
|
||||
ModelHelper.store_event.should == nil
|
||||
ModelHelper.hashed_label.should == nil
|
||||
ModelHelper.depth(:hour)
|
||||
ModelHelper.depth.should == :hour
|
||||
ModelHelper.store_event(true)
|
||||
ModelHelper.store_event.should == true
|
||||
ModelHelper.hashed_label(true)
|
||||
ModelHelper.hashed_label.should == true
|
||||
ModelHelper.options[:depth] = nil
|
||||
ModelHelper.options[:store_event] = nil
|
||||
ModelHelper.options[:hashed_label] = nil
|
||||
ModelHelper.depth.should == nil
|
||||
ModelHelper.store_event.should == nil
|
||||
ModelHelper.hashed_label.should == nil
|
||||
ModelHelper1.depth.should == nil
|
||||
ModelHelper1.store_event.should == nil
|
||||
ModelHelper1.hashed_label.should == nil
|
||||
ModelHelper1.depth(:hour)
|
||||
ModelHelper1.depth.should == :hour
|
||||
ModelHelper1.store_event(true)
|
||||
ModelHelper1.store_event.should == true
|
||||
ModelHelper1.hashed_label(true)
|
||||
ModelHelper1.hashed_label.should == true
|
||||
ModelHelper1.options[:depth] = nil
|
||||
ModelHelper1.options[:store_event] = nil
|
||||
ModelHelper1.options[:hashed_label] = nil
|
||||
ModelHelper1.depth.should == nil
|
||||
ModelHelper1.store_event.should == nil
|
||||
ModelHelper1.hashed_label.should == nil
|
||||
|
||||
ModelHelper4.class_name.should == "FancyHelper"
|
||||
ModelHelper4.send(:name).should == "FancyHelper"
|
||||
end
|
||||
|
||||
it "should store and fetch stats" do
|
||||
ModelHelper.store("sheep.black", {:count => 6, :weight => 461}, 4.hours.ago)
|
||||
ModelHelper.store("sheep.black", {:count => 2, :weight => 156})
|
||||
ModelHelper1.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4))
|
||||
ModelHelper1.store("sheep.black", {:count => 2, :weight => 156}, @time)
|
||||
|
||||
stats = ModelHelper.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["weight"].should == 156
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper.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[:weight].should == 617
|
||||
stats.first.should == stats.total
|
||||
|
||||
ModelHelper.store("sheep.white", {:count => 5, :weight => 393}, 4.hours.ago)
|
||||
ModelHelper.store("sheep.white", {:count => 4, :weight => 316})
|
||||
ModelHelper1.store("sheep.white", {:count => 5, :weight => 393}, @time.hours_ago(4))
|
||||
ModelHelper1.store("sheep.white", {:count => 4, :weight => 316}, @time)
|
||||
|
||||
stats = ModelHelper.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[:weight].should == 316
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper.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[:weight].should == 709
|
||||
stats.first.should == stats.total
|
||||
end
|
||||
|
||||
end
|
||||
it "should store and fetch grouping enabled stats" do
|
||||
ModelHelper1.store("sheep/black", {:count => 6, :weight => 461}, @time.hours_ago(4))
|
||||
ModelHelper1.store("sheep/black", {:count => 2, :weight => 156}, @time)
|
||||
ModelHelper1.store("sheep/white", {:count => 5, :weight => 393}, @time.hours_ago(4))
|
||||
ModelHelper1.store("sheep/white", {:count => 4, :weight => 316}, @time)
|
||||
|
||||
stats = ModelHelper1.fetch("sheep/black", @time.hours_ago(2), @time.hours_since(1))
|
||||
stats.total["count"].should == 2
|
||||
stats.total["weight"].should == 156
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper1.fetch("sheep/black", @time.hours_ago(5), @time.hours_since(1))
|
||||
stats.total[:count].should == 8
|
||||
stats.total[:weight].should == 617
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper1.fetch("sheep/white", @time.hours_ago(2), @time.hours_since(1))
|
||||
stats.total[:count].should == 4
|
||||
stats.total[:weight].should == 316
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper1.fetch("sheep/white", @time.hours_ago(5), @time.hours_since(1))
|
||||
stats.total[:count].should == 9
|
||||
stats.total[:weight].should == 709
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper1.fetch("sheep", @time.hours_ago(2), @time.hours_since(1))
|
||||
stats.total[:count].should == 6
|
||||
stats.total[:weight].should == 472
|
||||
stats.first.should == stats.total
|
||||
|
||||
stats = ModelHelper1.fetch("sheep", @time.hours_ago(5), @time.hours_since(1))
|
||||
stats.total[:count].should == 17
|
||||
stats.total[:weight].should == 1326
|
||||
stats.first.should == stats.total
|
||||
end
|
||||
|
||||
it "should connect to different Redis servers on a per-model basis" do
|
||||
ModelHelper3.redis.client.db.should == 14
|
||||
|
||||
ModelHelper3.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4), :label_indexing => false)
|
||||
ModelHelper3.store("sheep.black", {:count => 2, :weight => 156}, @time, :label_indexing => false)
|
||||
|
||||
db.keys("*").should be_empty
|
||||
ModelHelper1.redis.keys("*").should be_empty
|
||||
db("ModelHelper3").keys("*").should have(5).items
|
||||
ModelHelper3.redis.keys("*").should have(5).items
|
||||
|
||||
stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(2), @time.hours_since(1), :label_indexing => false)
|
||||
stats.total["count"].should == 2
|
||||
stats.total["weight"].should == 156
|
||||
stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
|
||||
stats.total[:count].should == 8
|
||||
stats.total[:weight].should == 617
|
||||
|
||||
ModelHelper3.connect_to(:port => 8379, :db => 13)
|
||||
ModelHelper3.redis.client.db.should == 13
|
||||
|
||||
stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
|
||||
stats.total.should == {}
|
||||
|
||||
ModelHelper3.connect_to(:port => 8379, :db => 14)
|
||||
ModelHelper3.redis.client.db.should == 14
|
||||
|
||||
stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
|
||||
stats.total[:count].should == 8
|
||||
stats.total[:weight].should == 617
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
36
spec/options_spec.rb
Normal file
36
spec/options_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe Redistat::Options do
|
||||
|
||||
before(:each) do
|
||||
@helper = OptionsHelper.new
|
||||
@helper.parse_options(:wtf => 'dude', :foo => 'booze')
|
||||
end
|
||||
|
||||
it "should #parse_options" do
|
||||
@helper.options[:hello].should == 'world'
|
||||
@helper.options[:foo].should == 'booze'
|
||||
@helper.options[:wtf].should == 'dude'
|
||||
@helper.raw_options.should_not have_key(:hello)
|
||||
end
|
||||
|
||||
it "should create option_accessors" do
|
||||
@helper.hello.should == 'world'
|
||||
@helper.hello('woooo')
|
||||
@helper.hello.should == 'woooo'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class OptionsHelper
|
||||
include Redistat::Options
|
||||
|
||||
option_accessor :hello
|
||||
|
||||
def default_options
|
||||
{ :hello => 'world',
|
||||
:foo => 'bar' }
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -8,5 +8,5 @@ require 'rspec'
|
||||
require 'rspec/autorun'
|
||||
|
||||
# use the test Redistat instance
|
||||
Redistat.connect({:port => 8379, :db => 15})
|
||||
Redistat.flush
|
||||
Redistat.connect(:port => 8379, :db => 15, :thread_safe => true)
|
||||
Redistat.redis.flushdb
|
||||
|
||||
@@ -46,27 +46,87 @@ describe Redistat::Summary do
|
||||
end
|
||||
end
|
||||
|
||||
it "should update summaries even if no label is set" do
|
||||
key = Redistat::Key.new(@scope, nil, @date, {:depth => :day})
|
||||
Redistat::Summary.update(key, @stats, :hour)
|
||||
summary = db.hgetall(key.to_s(:hour))
|
||||
summary.should have(2).items
|
||||
summary["views"].should == "3"
|
||||
summary["visitors"].should == "2"
|
||||
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, {:enable_grouping => 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.to_s.should == "views/about_us"
|
||||
key.groups[1].label.to_s.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.to_s.should == "views/contact"
|
||||
key.groups[1].label.to_s.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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
39
spec/thread_safety_spec.rb
Normal file
39
spec/thread_safety_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user