mirror of
https://github.com/jimeh/redistat.git
synced 2026-02-19 13:26:39 +00:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9afd5b6ccf | |||
| f89ccc2514 | |||
| ef7b74df81 | |||
| 8106677561 | |||
| 00e0015ac2 | |||
| 7e82246662 | |||
| 84f3bf26b5 | |||
| 2e2d3273cc | |||
| a983e554c6 | |||
| 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 | |||
| bc7a563f20 | |||
| 39da4c96a7 | |||
| fa7903dc7a | |||
| 8a0e1a47a2 | |||
| 8e3e54a027 | |||
| 07ae8b5c78 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -16,10 +16,12 @@ tmtags
|
||||
## PROJECT::GENERAL
|
||||
coverage
|
||||
rdoc
|
||||
pkg
|
||||
pkg/*
|
||||
*.gem
|
||||
.bundle
|
||||
Gemfile.lock
|
||||
|
||||
## PROJECT::SPECIFIC
|
||||
.bundle/*
|
||||
.yardoc/*
|
||||
spec/db/*
|
||||
doc
|
||||
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
|
||||
|
||||
205
README.md
205
README.md
@@ -1,15 +1,206 @@
|
||||
# Redistat
|
||||
# Redistat #
|
||||
|
||||
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 (Crash Course) ##
|
||||
|
||||
view\_stats.rb:
|
||||
|
||||
require 'redistat'
|
||||
|
||||
class ViewStats
|
||||
include Redistat::Model
|
||||
end
|
||||
|
||||
|
||||
## Note on Patches/Pull Requests
|
||||
### Simple Example ###
|
||||
|
||||
Store:
|
||||
|
||||
ViewStats.store('hello', {:world => 4})
|
||||
ViewStats.store('hello', {:world => 2}, 2.hours.ago)
|
||||
|
||||
Fetch:
|
||||
|
||||
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).all
|
||||
#=> [{'world' => 4}]
|
||||
ViewStats.find('hello', 1.hour.ago, 1.hour.from_now).total
|
||||
#=> {'world' => 4}
|
||||
ViewStats.find('hello', 3.hour.ago, 1.hour.from_now).total
|
||||
#=> {'world' => 6}
|
||||
|
||||
|
||||
### Advanced Example ###
|
||||
|
||||
Store page view on product #44 from Chrome 11:
|
||||
|
||||
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
|
||||
|
||||
Fetch product #44 stats:
|
||||
|
||||
ViewStats.find('views/product/44', 23.hours.ago, 1.hour.from_now).total
|
||||
#=> { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
|
||||
|
||||
Store a page view on product #32 from Firefox 3:
|
||||
|
||||
ViewStats.store('views/product/32', {'count/firefox/3' => 1})
|
||||
|
||||
Fetch product #32 stats:
|
||||
|
||||
ViewStats.find('views/product/32', 23.hours.ago, 1.hour.from_now).total
|
||||
#=> { 'count' => 1, 'count/firefox' => 1, 'count/firefox/3' => 1 }
|
||||
|
||||
Fetch stats for all products:
|
||||
|
||||
ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now).total
|
||||
#=> { 'count' => 2,
|
||||
# 'count/chrome' => 1,
|
||||
# 'count/chrome/11' => 1,
|
||||
# 'count/firefox' => 1,
|
||||
# 'count/firefox/3' => 1 }
|
||||
|
||||
Store a 404 error view:
|
||||
|
||||
ViewStats.store('views/error/404', {'count/chrome/9' => 1})
|
||||
|
||||
Fetch stats for all views across the board:
|
||||
|
||||
ViewStats.find('views', 23.hours.ago, 1.hour.from_now).total
|
||||
#=> { 'count' => 3,
|
||||
# 'count/chrome' => 2,
|
||||
# 'count/chrome/9' => 1,
|
||||
# 'count/chrome/11' => 1,
|
||||
# 'count/firefox' => 1,
|
||||
# 'count/firefox/3' => 1 }
|
||||
|
||||
Fetch list of products known to Redistat:
|
||||
|
||||
finder = ViewStats.find('views/product', 23.hours.ago, 1.hour.from_now)
|
||||
finder.children.map { |child| child.label.me }
|
||||
#=> [ "32", "44" ]
|
||||
finder.children.map { |child| child.label.to_s }
|
||||
#=> [ "views/products/32", "views/products/44" ]
|
||||
finder.children.map { |child| child.total }
|
||||
#=> [ { "count" => 1, "count/firefox" => 1, "count/firefox/3" => 1 },
|
||||
# { "count" => 1, "count/chrome" => 1, "count/chrome/11" => 1 } ]
|
||||
|
||||
|
||||
## Terminology ##
|
||||
|
||||
### Scope ###
|
||||
|
||||
A type of global-namespace for storing data. When using the `Redistat::Model` wrapper, the scope is automatically set to the class name. In the examples above, the scope is `ViewStats`. Can be overridden by calling the `#scope` class method on your model class.
|
||||
|
||||
### Label ###
|
||||
|
||||
Identifier string to separate different types and groups of statistics from each other. The first argument of the `#store`, `#find`, and `#fetch` methods is the label that you're storing to, or fetching from.
|
||||
|
||||
Labels support multiple grouping levels by splitting the label string with `/` and storing the same stats for each level. For example, when storing data to a label called `views/product/44`, the data is stored for the label you specify, and also for `views/product` and `views`.
|
||||
|
||||
A word of caution: Don't use a crazy number of group levels. As two levels causes twice as many `hincrby` calls to Redis as not using the grouping feature. Hence using 10 grouping levels, causes 10 times as many write calls to Redis.
|
||||
|
||||
### Input Statistics Data ###
|
||||
|
||||
You provide Redistat with the data you want to store using a Ruby Hash. This data is then stored in a corresponding Redis hash with identical key/field names.
|
||||
|
||||
Key names in the hash also support grouping features similar to those available for Labels. Again, the more levels you use, the more write calls to Redis, so avoid using 10-15 levels.
|
||||
|
||||
### Depth (Storage Accuracy) ###
|
||||
|
||||
Define how accurately data should be stored, and how accurately it's looked up when fetching it again. By default Redistat uses a depth value of `:hour`, which means it's impossible to separate two events which were stored at 10:18 and 10:23. In Redis they are both stored within a date key of `2011031610`.
|
||||
|
||||
You can set depth within your model using the `#depth` class method. Available depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`
|
||||
|
||||
### Time Ranges ###
|
||||
|
||||
When you fetch data, you need to specify a start and an end time. The selection behavior can seem a bit weird at first when, but makes sense when you understand how Redistat works internally.
|
||||
|
||||
For example, if we are using a Depth value of `:hour`, and we trigger a fetch call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats from 13:00:00 till 13:59:59 are returned, as they were all stored within the key for the 13th hour. If both 13:00 and 14:00 was returned, you would get results from two hole hours. Hence if you want up to the second data, use an end time of `1.hour.from_now`.
|
||||
|
||||
### The Finder Object ###
|
||||
|
||||
Calling the `#find` method on a Redistat model class returns a `Redistat::Finder` object. The finder is a lazy-loaded gateway to your data. Meaning you can create a new finder, and modify instantiated finder's label, scope, dates, and more. It does not call Redis and fetch the data until you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the finder.
|
||||
|
||||
This section does need further expanding as there's a lot to cover when it comes to the finder.
|
||||
|
||||
|
||||
|
||||
## Internals ##
|
||||
|
||||
### Storing / Writing ###
|
||||
|
||||
Redistat stores all data into a Redis hash keys. The Redis key name the used consists of three parts. The scope, label, and datetime:
|
||||
|
||||
{scope}/{label}:{datetime}
|
||||
|
||||
For example, this...
|
||||
|
||||
ViewStats.store('views/product/44', {'count/chrome/11' => 1})
|
||||
|
||||
...would store the follow hash of data...
|
||||
|
||||
{ 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
|
||||
|
||||
...to all 12 of these Redis hash keys...
|
||||
|
||||
ViewStats/views:2011
|
||||
ViewStats/views:201103
|
||||
ViewStats/views:20110315
|
||||
ViewStats/views:2011031510
|
||||
ViewStats/views/product:2011
|
||||
ViewStats/views/product:201103
|
||||
ViewStats/views/product:20110315
|
||||
ViewStats/views/product:2011031510
|
||||
ViewStats/views/product/44:2011
|
||||
ViewStats/views/product/44:201103
|
||||
ViewStats/views/product/44:20110315
|
||||
ViewStats/views/product/44:2011031510
|
||||
|
||||
...by creating the Redis key, and/or hash field if needed, otherwise it simply increments the already existing data.
|
||||
|
||||
It would also create the following Redis sets to keep track of which child labels are available:
|
||||
|
||||
ViewStats.label_index:
|
||||
ViewStats.label_index:views
|
||||
ViewStats.label_index:views/product
|
||||
|
||||
It should now be more obvious to you why you should think about how you use the grouping capabilities so you don't go crazy and use 10-15 levels. Storing is done through Redis' `hincrby` call, which only supports a single key/field combo. Meaning the above example would call `hincrby` a total of 36 times to store the data, and `sadd` a total of 3 times to ensure the label index is accurate. 39 calls is however not a problem for Redis, most calls happen in less than 0.15ms (0.00015 seconds) on my local machine.
|
||||
|
||||
|
||||
### Fetching / Reading ###
|
||||
|
||||
By default when fetching statistics, Redistat will figure out how to do the least number of reads from Redis. First it checks how long range you're fetching. If whole days, months or years for example fit within the start and end dates specified, it will fetch the one key for the day/month/year in question. It further drills down to the smaller units.
|
||||
|
||||
It is also intelligent enough to not fetch each day from 3-31 of a month, instead it would fetch the data for the whole month and the first two days, which are then removed from the summary of the whole month. This means three calls to `hgetall` instead of 29 if each whole day was fetched.
|
||||
|
||||
|
||||
## Todo ##
|
||||
|
||||
* More details in 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 ##
|
||||
|
||||
* Fork the project.
|
||||
* Make your feature addition or bug fix.
|
||||
@@ -20,9 +211,9 @@ For now, please check `spec/model_spec.rb` and `spec/model_helper.rb` to get sta
|
||||
* Send me a pull request. Bonus points for topic branches.
|
||||
|
||||
|
||||
## License and Copyright
|
||||
## License and Copyright ##
|
||||
|
||||
Copyright (c) 2010 Jim Myhrberg.
|
||||
Copyright (c) 2011 Jim Myhrberg.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
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 = 'TODO: one-line summary of your gem'
|
||||
gem.description = 'TODO: longer description of your gem'
|
||||
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
|
||||
|
||||
attr_accessor :scope
|
||||
attr_accessor :date
|
||||
attr_accessor :options
|
||||
|
||||
def initialize(scope, label = nil, date = nil, options = {})
|
||||
@scope = scope
|
||||
self.label = label if !label.nil?
|
||||
self.date = date ||= Time.now
|
||||
@options = default_options.merge(options ||= {})
|
||||
end
|
||||
include Database
|
||||
include Options
|
||||
|
||||
def default_options
|
||||
{ :depth => :day }
|
||||
{ :depth => :hour }
|
||||
end
|
||||
|
||||
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 prefix
|
||||
key = "#{@scope}"
|
||||
key << "/" + ((@options[:label_hash].nil? || @options[:label_hash] == true) ? @label.hash : @label.name) if !label.nil?
|
||||
key << "/#{label.name}" if !label.nil?
|
||||
key << ":"
|
||||
key
|
||||
end
|
||||
@@ -26,21 +24,53 @@ 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)
|
||||
def parent
|
||||
@parent ||= self.class.new(self.scope, @label.parent, self.date, @options) unless @label.parent.nil?
|
||||
end
|
||||
|
||||
def children
|
||||
members = db.smembers("#{scope}#{LABEL_INDEX}#{@label}") || [] # older versions of Redis returns nil
|
||||
members.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,26 +1,68 @@
|
||||
module Redistat
|
||||
class Label
|
||||
include Database
|
||||
include Options
|
||||
|
||||
attr_reader :name
|
||||
attr_reader :hash
|
||||
def default_options
|
||||
{ :hashed_label => false }
|
||||
end
|
||||
|
||||
def initialize(str)
|
||||
@name = str.to_s
|
||||
@hash = Digest::SHA1.hexdigest(@name)
|
||||
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 : self.to_s
|
||||
end
|
||||
|
||||
def hash
|
||||
@hash ||= Digest::SHA1.hexdigest(self.to_s)
|
||||
end
|
||||
|
||||
def save
|
||||
@saved = (db.set("#{KEY_LEBELS}#{@hash}", @name) == "OK")
|
||||
@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)
|
||||
self.new(name).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,50 +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 depth(depth = nil)
|
||||
if !depth.nil?
|
||||
options[:depth] = depth
|
||||
else
|
||||
options[:depth] || 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 store_event(boolean = nil)
|
||||
if !boolean.nil?
|
||||
options[:store_event] = boolean
|
||||
else
|
||||
options[:store_event] || nil
|
||||
end
|
||||
def find_event(event_id)
|
||||
Event.find(self.name, event_id)
|
||||
end
|
||||
|
||||
def options
|
||||
@options ||= {}
|
||||
|
||||
#
|
||||
# options methods
|
||||
#
|
||||
|
||||
option_accessor :depth
|
||||
option_accessor :scope
|
||||
option_accessor :store_event
|
||||
option_accessor :hashed_label
|
||||
option_accessor :label_indexing
|
||||
|
||||
alias :class_name :scope
|
||||
|
||||
def connect_to(opts = {})
|
||||
Connection.create(opts.merge(:ref => name))
|
||||
options[:connection_ref] = name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
#
|
||||
# resource access methods
|
||||
#
|
||||
|
||||
def connection
|
||||
db(options[:connection_ref])
|
||||
end
|
||||
alias :redis :connection
|
||||
|
||||
def name
|
||||
@name ||= self.to_s
|
||||
options[:scope] || (@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.6"
|
||||
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,53 +13,117 @@ 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
|
||||
|
||||
it "should convert to string properly" do
|
||||
@key.to_s.should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(:hour)}"
|
||||
@key.to_s.should == "#{@scope}/#{@label}:#{@key.date.to_s(:hour)}"
|
||||
props = [:year, :month, :day, :hour, :min, :sec]
|
||||
props.each do
|
||||
@key.to_s(props.last).should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(props.last)}"
|
||||
@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 hash_label option" do
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :label_hash => true})
|
||||
it "should abide to hashed_label option" do
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :hashed_label => true})
|
||||
@key.to_s.should == "#{@scope}/#{@label_hash}:#{@key.date.to_s(:hour)}"
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :label_hash => false})
|
||||
@key = Redistat::Key.new(@scope, @label, @date, {:depth => :hour, :hashed_label => false})
|
||||
@key.to_s.should == "#{@scope}/#{@label}:#{@key.date.to_s(:hour)}"
|
||||
end
|
||||
|
||||
it "should have default depth option" do
|
||||
@key = Redistat::Key.new(@scope, @label, @date)
|
||||
@key.depth.should == :day
|
||||
@key.depth.should == :hour
|
||||
end
|
||||
|
||||
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
|
||||
@@ -15,14 +15,57 @@ describe Redistat::Label do
|
||||
end
|
||||
|
||||
it "should store a label hash lookup key" do
|
||||
@label.save
|
||||
@label.saved?.should be_true
|
||||
db.get("#{Redistat::KEY_LEBELS}#{@label.hash}").should == @name
|
||||
label = Redistat::Label.new(@name, {:hashed_label => true}).save
|
||||
label.saved?.should be_true
|
||||
db.hget(Redistat::KEY_LABELS, label.hash).should == @name
|
||||
|
||||
@name = "contact_us"
|
||||
@label = Redistat::Label.create(@name)
|
||||
@label.saved?.should be_true
|
||||
db.get("#{Redistat::KEY_LEBELS}#{@label.hash}").should == @name
|
||||
name = "contact_us"
|
||||
label = Redistat::Label.create(name, {:hashed_label => true})
|
||||
label.saved?.should be_true
|
||||
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
|
||||
|
||||
|
||||
@@ -11,5 +11,20 @@ class ModelHelper2
|
||||
|
||||
depth :day
|
||||
store_event true
|
||||
hashed_label true
|
||||
|
||||
end
|
||||
|
||||
class ModelHelper3
|
||||
include Redistat::Model
|
||||
|
||||
connect_to :port => 8379, :db => 14
|
||||
|
||||
end
|
||||
|
||||
class ModelHelper4
|
||||
include Redistat::Model
|
||||
|
||||
scope "FancyHelper"
|
||||
|
||||
end
|
||||
@@ -5,56 +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.scope.should be_nil
|
||||
|
||||
ModelHelper.depth.should == nil
|
||||
ModelHelper.store_event.should == nil
|
||||
ModelHelper.depth(:hour)
|
||||
ModelHelper.depth.should == :hour
|
||||
ModelHelper.store_event(true)
|
||||
ModelHelper.store_event.should == true
|
||||
ModelHelper.options[:depth] = nil
|
||||
ModelHelper.options[:store_event] = nil
|
||||
ModelHelper.depth.should == nil
|
||||
ModelHelper.store_event.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.scope.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