168 Commits

Author SHA1 Message Date
01a39b1b20 Merge branch 'release/v0.3.0' 2011-04-18 14:27:59 +01:00
ae5a391012 started release v0.3.0 2011-04-18 14:27:40 +01:00
c53c7116dd updated Connection TODO comment 2011-04-18 14:26:52 +01:00
d9a8aefcc5 updated readme with thread_safe and buffer info 2011-04-18 14:25:38 +01:00
0ec2f5bd14 Merge branch 'feature/buffer' into dev 2011-04-18 14:11:06 +01:00
b2c31a0e87 ensure buffer size value is read/written to in a thread-safe manner 2011-04-18 12:40:49 +01:00
b13da6f332 create a flush buffer #at_exit callback to ensure any buffered messages are flushed to Redis on process exit 2011-04-18 12:37:24 +01:00
7b5c308960 model spec updated to test write buffer 2011-04-18 12:36:43 +01:00
eb1d607a61 a number of issues fixed with Buffer class, and specs updated accordingly 2011-04-18 12:36:24 +01:00
b129074cd7 make Buffer#queue a private method as it's never supposed to be modified or read from outside of the Buffer object 2011-04-18 10:00:22 +01:00
4b06513813 additional specs for Redistat::Buffer, still a few more needed 2011-04-15 17:42:48 +01:00
2ca5aae4b8 require required libraries, just cause 2011-04-15 16:46:12 +01:00
6c63843cd5 updated Redistat::Summary to incorporate use of write Buffer 2011-04-15 16:45:56 +01:00
3a25fcc788 created Redistat::Buffer, mainly feature complete, still needs a few more specs 2011-04-15 16:45:21 +01:00
61231a8b57 updated Redistat::Summary to make it easier to plugin the buffer interception code 2011-04-15 14:14:17 +01:00
a197a04ce8 moved all internal mixin modules to lib/redistat/mixins to tidy up the file structure a bit 2011-04-15 14:10:51 +01:00
5d92c1dbae created Redistat::Synchronize mixin to help with thread-safety 2011-04-15 14:03:26 +01:00
0a7abe935e thread-safe connection handler 2011-04-14 16:53:29 +01:00
f155f6db05 cleaned up Connection spec a bit 2011-04-14 16:50:43 +01:00
9afd5b6ccf Merge branch 'release/v0.2.6' 2011-04-13 10:29:02 +01:00
eb0c461aa7 Merge branch 'release/v0.2.6' into dev 2011-04-13 10:29:02 +01:00
f89ccc2514 started release v0.2.6 2011-04-13 10:25:52 +01:00
ef7b74df81 Fixed an issue caused by smembers returning nil rather than an empty Array in some older versions of Redis. 2011-04-13 10:25:13 +01:00
8106677561 Merge branch 'release/v0.2.5' into dev 2011-03-16 00:58:52 +00:00
00e0015ac2 Merge branch 'release/v0.2.5' 2011-03-16 00:58:43 +00:00
7e82246662 started release v0.2.5 2011-03-16 00:58:31 +00:00
84f3bf26b5 updated readme 2011-03-16 00:57:18 +00:00
2e2d3273cc set #scope rather than #class_name as the preferred method for setting the scope value within a Redistat::Model 2011-03-16 00:56:59 +00:00
a983e554c6 Merge branch 'release/v0.2.4' into dev 2011-03-14 11:14:06 +00:00
ecbd15ef1e Merge branch 'release/v0.2.4' 2011-03-14 11:14:01 +00:00
2b2461dd9f started release v0.2.4 2011-03-14 11:13:50 +00:00
3e177c9ae4 changed json dependency back to '>= 1.4.0' for backwards compatibility with certain older projects/gems, make sure you're using 1.5.0 or later of the json gem for JRuby support 2011-03-14 11:13:06 +00:00
d4cd5402bc create Label#join method for easily joining Labels 2011-03-14 11:10:24 +00:00
d560a7deff killed an old line of commented out code 2011-03-14 10:38:16 +00:00
3df6666704 additions to specs 2011-03-14 10:37:56 +00:00
80fd63059b updated time_ext dependency to ensure there's no loading issues within Rails applications 2011-03-13 23:47:02 +00:00
d0b7f097a8 updated json dependency to '>= 1.5.0' to ensure JRuby compatability 2011-03-13 23:46:00 +00:00
875d16b01c Merge branch 'release/v0.2.3' into dev 2011-03-13 20:30:21 +00:00
00791d36d8 Merge branch 'release/v0.2.3' 2011-03-13 20:30:17 +00:00
94b589c5e6 started release v0.2.3 2011-03-13 20:30:11 +00:00
cdf52869d6 added #find_event method to Model 2011-03-13 20:28:17 +00:00
8b711d4d9c fixed a bug with Event#find 2011-03-13 20:24:06 +00:00
e4aaedfe58 made Key#scope return Scope object instead of Scope#to_s 2011-03-13 20:23:41 +00:00
ea820d44f4 fixed typo in Finder spec 2011-03-13 19:52:30 +00:00
acedf071d1 improved options passed into Finder object, :depth option is not needed if :interval is set to a depth value instead of true 2011-03-13 19:51:04 +00:00
108b6ab02e Finder's options methods now set the option when an argument is supplied and returns self for method chaining. When no argument is supplied it returns the option value itself.
Example:

    finder = Redistat::Finder.new
    finder.scope("Foo") #=> Finder object
    finder.scope        #=> Scope object
    finder.scope.to_s   #=> "Foo"
2011-03-13 19:46:52 +00:00
9920c0dc2f Merge branch 'release/v0.2.2' into dev 2011-03-12 22:26:58 +00:00
a72ad31c51 Merge branch 'release/v0.2.2' 2011-03-12 22:26:52 +00:00
008228660e started release v0.2.2 2011-03-12 22:26:12 +00:00
b8ddcdf71a Merge remote-tracking branch 'origin/dev' into dev 2011-03-12 22:25:35 +00:00
94fcd5b4ae fixed a ruby 1.9.x issue 2011-03-12 22:24:06 +00:00
e2a551d01c syntax beautification 2011-03-12 22:23:06 +00:00
43fc8bc2dd updated Hash extensions and specs 2011-03-12 22:21:15 +00:00
dcca3556ea some cleanup 2011-03-12 22:00:21 +00:00
d5f79b82a9 somewhat better loading procedure 2011-03-12 21:59:52 +00:00
0938781cd1 extend ::Hash with #set_or_incr and #merge_and_incr methods 2011-03-12 21:47:54 +00:00
82119fcf69 wrong usage of interval method in Finder spec 2011-03-11 15:38:10 +00:00
746d0fea8f Merge branch 'release/v0.2.1' into dev 2011-03-10 16:31:42 +00:00
331bf81e3a Merge branch 'release/v0.2.1' 2011-03-10 16:31:34 +00:00
f2c026c1eb started release v0.2.1 2011-03-10 16:27:24 +00:00
57517983f6 added #parent method to Finder objects 2011-03-10 16:26:38 +00:00
53aee885bd removed defunct TODO comment 2011-03-10 10:42:24 +00:00
8001a98a26 fixed a typo... ffs... 2011-03-10 10:42:10 +00:00
34331a655e Merge branch 'release/v0.2.0' into dev 2011-03-10 00:49:22 +00:00
91ad5b2d3c Merge branch 'release/v0.2.0' 2011-03-10 00:49:18 +00:00
9fd5ae8545 started release v0.2.0 2011-03-10 00:49:07 +00:00
14b7f4768e added credits section to readme 2011-03-10 00:47:14 +00:00
57274ffb21 updated reverse label hash lookup storage format, which might be a pain if you have been using the hashed_label option 2011-03-10 00:46:55 +00:00
8d063c98e5 Merge branch 'feature/options' into dev 2011-03-10 00:27:26 +00:00
d39d5d8dde most components use new Options helper module 2011-03-10 00:27:13 +00:00
3a00353f83 created Options module to help organize the multiple options passed from one object to another 2011-03-10 00:24:35 +00:00
49fc2afcfd added a FIXME comment about broken model spec till index_labels option is implemented 2011-03-09 22:50:04 +00:00
cfbe58a509 support indexing top-level labels too 2011-03-09 22:48:27 +00:00
629f46ed89 Merge branch 'feature/label-indexing' into dev 2011-03-09 17:28:46 +00:00
9faa0db7b8 drastic change in label indexing 2011-03-09 17:05:10 +00:00
834614ab79 added ruby-debug to development dependencies 2011-03-09 15:57:12 +00:00
47a1b0737c better readability 2011-03-09 11:57:34 +00:00
5d3c181641 update Label index when saving Label object if grouping is enabled and used 2011-03-09 11:57:18 +00:00
e3f23433d9 cleaned up #sub_labels feature in Label object 2011-03-09 11:56:42 +00:00
482253f517 make Finder work with Scope and Label objects rather than strings 2011-03-09 11:55:42 +00:00
66b9f4e949 less code duplication 2011-03-09 11:54:51 +00:00
7a28d1210f added a todo item about a typo 2011-03-09 11:00:08 +00:00
d74dc41110 added label indexing features when using label groupings 2011-03-09 10:59:53 +00:00
ac338bb4f0 added #parent_group method to Label and Key objects 2011-03-09 10:25:37 +00:00
325a264411 Merge branch 'release/v0.1.1' into dev 2011-03-09 01:34:55 +00:00
dc3816f691 Merge branch 'release/v0.1.1' 2011-03-09 01:34:51 +00:00
57d8fddd23 started release v0.1.1 2011-03-09 01:34:12 +00:00
c5e9c02c84 Merge branch 'feature/lazy-loading' into dev 2011-03-09 01:31:34 +00:00
e0eac61a59 updated Model to take advantage of new lazy-loading features 2011-03-09 01:31:20 +00:00
33e9477552 finalized lazy-loading work on Finder 2011-03-09 01:25:09 +00:00
7e8e1dacc7 initial work and specs to properly support lazy-loading results from Finder objects 2011-03-08 01:30:48 +00:00
06cd30a20c fixed Model spec as it was failing at certain times of the day 2011-03-08 01:03:56 +00:00
f481554fc9 updated copyright year 2011-03-08 00:44:10 +00:00
a9b6f2c99a Merge branch 'release/v0.1.0' into dev 2011-03-04 17:41:45 +00:00
8710f4a51f Merge branch 'release/v0.1.0' 2011-03-04 17:41:39 +00:00
96e9b0a736 Version bump to 0.1.0 2011-03-04 17:41:14 +00:00
102fb41a6b Merge branch 'feature/grouping' into dev 2011-03-04 17:40:17 +00:00
b0a44a6abc some more sanity checks to Label spec 2011-03-04 17:40:02 +00:00
f8dfb034af added label grouping to Key and Summary classes 2011-03-04 17:39:51 +00:00
15904e8a94 added grouping support to Redistat::Label 2011-03-04 16:25:31 +00:00
fe221c3f31 added enable_grouping option to disable grouping features, enabled by default 2011-03-04 13:02:20 +00:00
7b1feda061 added key grouping for statistics Hash
Example:
store(“message”, {“count/private” => 1})
store(“message”, {“count/public” => 1})
fetch("message", 2.minutes.ago, Time.now)
  #=> { "count" => 2,
        "count/private" => 1,
        "count/public" => 1 }
2011-03-04 12:54:50 +00:00
968ef47ac5 fixed name of SystemTimer gem in readme 2011-03-04 12:17:55 +00:00
e3c4a5da9a added Gemfile.lock to .gitignore file 2011-03-04 12:17:39 +00:00
b215c6d56c made rcov rake task work 2011-03-04 12:16:49 +00:00
8d5c73a539 Merge branch 'release/v0.0.9' into dev 2011-02-23 18:11:54 +00:00
0d5170bc26 Merge branch 'release/v0.0.9' 2011-02-23 18:11:50 +00:00
4692780d1e Version bump to 0.0.9 2011-02-23 18:11:15 +00:00
f8ec626762 removed Gemfile.lock from git repo (this is a gem
not an application ^_^)
2011-02-23 18:10:09 +00:00
ec54385192 updated activesupport dependency to ">= 2.3.6" as
redistat breaks with earlier versions
2011-02-23 18:09:19 +00:00
4808a97d19 added .rvmrc file for development ease 2011-02-23 18:08:21 +00:00
1dce2780e0 Merge branch 'Oscil8-master' into dev 2011-02-23 18:00:16 +00:00
Ariel Salomon
cab7ed5633 Fix for use with Active Support 2.3.x 2011-02-23 07:42:58 -08:00
861d040187 Merge branch 'release/v0.0.8' into dev 2011-01-12 16:13:18 +00:00
3267ee1eb9 Merge branch 'release/v0.0.8' 2011-01-12 16:13:13 +00:00
6309e4b217 Version bump to 0.0.8 2011-01-12 16:12:55 +00:00
776ee8ac97 make version available in code via
Redistat::VERSION
2011-01-12 16:11:19 +00:00
3b346e88e0 moved DateHelper module to it's own files for the
sake of transparency
2011-01-12 16:10:38 +00:00
c3fe861b10 connection handling was so thread-safe that it
stopped working in newly created threads
2011-01-12 16:04:42 +00:00
bc5034b6bb Merge branch 'release/v0.0.7' into dev 2010-12-29 17:29:20 +00:00
66510fe344 Merge branch 'release/v0.0.7' 2010-12-29 17:29:13 +00:00
745473862f added note about system_timer gem to readme 2010-12-29 17:28:56 +00:00
a5c8fc6fbf Version bump to 0.0.7 2010-12-29 17:27:19 +00:00
115b223d7c added class_name option to Model warpper for
customizing the scope used in Redis keys
2010-12-29 17:26:22 +00:00
0597b587fd Merge branch 'feature/ruby19' into dev 2010-12-29 17:14:40 +00:00
89932759ef Merge branch 'master' of https://github.com/JamesHarrison/redistat into feature/ruby19 2010-12-29 16:42:50 +00:00
55e0687837 Merge branch 'dev' into feature/ruby19 2010-12-29 16:42:36 +00:00
James Harrison
93360dbeb9 Specs pass again - problem with Time.now resolution surpassing that provided by values stored in a Redistat::Date, truncated to seconds by using to_s, which is accurate enough for testing purposes 2010-12-28 23:55:39 +00:00
James Harrison
6a66605e0b Adds Ruby 1.9.2 compat (references to TimeExt#round collide with the new real Time#now, changed to TimeExt#beginning_of_closest), 3 specs failing relating to time equality testing 2010-12-28 23:46:25 +00:00
James Harrison
d9ce0daade Removes SystemTimer for Ruby 1.9.2 compat 2010-12-28 22:11:33 +00:00
f6ec2e97b2 Merge branch 'dev' of github.com:jimeh/redistat into dev 2010-12-09 22:42:55 +00:00
67dc9433c7 fixed typo in deprecation warning 2010-12-09 22:42:27 +00:00
f0fcd2110d Merge branch 'release/v0.0.6' into dev 2010-12-01 13:41:48 +00:00
24112e4705 Merge branch 'release/v0.0.6' 2010-12-01 13:41:44 +00:00
b9752ff92f Version bump to 0.0.6 2010-12-01 13:41:30 +00:00
14a093d79b updated gem dependencies to less specific versions
as older versions should work
2010-12-01 13:40:55 +00:00
84a05363dd Merge branch 'release/v0.0.5' into dev 2010-11-28 11:56:23 +00:00
690d1d9407 Merge branch 'release/v0.0.5' 2010-11-28 11:56:15 +00:00
2aedd4eee3 Version bump to 0.0.5 2010-11-28 11:55:50 +00:00
f906cf068e added a spec for Collection#total 2010-11-28 11:52:23 +00:00
cbb9050c80 fixed a typo 2010-11-28 11:51:50 +00:00
58a2fb560c Merge branch 'feature/multi-conf' into dev 2010-11-28 11:48:27 +00:00
6bae8ce2bc Updated Connection spec so the specs actually pass
without a local Redis server running on the
default port of 6379.
2010-11-28 11:48:10 +00:00
18e6125c6a Added support for connection_ref's down throughout
the code, so models can connect to specific Redis
servers.

I believe a lot of the code needs some
restructuring at some point down the line to
handle multiple connections in a cleaner way, but
for now it'll do.
2010-11-28 11:47:26 +00:00
dc162e0c89 initial work to being able to use per-model redis
configurations
2010-11-28 10:10:58 +00:00
5338676a5f moved all dependencies to gemspec 2010-11-25 22:48:57 +00:00
02fe41082a Merge branch 'release/v0.0.4' into dev 2010-11-24 13:51:13 +00:00
65e7745419 Merge branch 'release/v0.0.4' 2010-11-24 13:51:03 +00:00
490356ee96 Bumped version to 0.0.4 2010-11-24 13:50:54 +00:00
a6c4600aa5 Merge branch 'feature/new_gemspec' into dev 2010-11-24 13:49:51 +00:00
85ba61b2cc removed jeweler's VERSION file 2010-11-24 13:48:48 +00:00
8f6a4a6820 fixed a typo 2010-11-24 13:47:51 +00:00
81ee2ec0b6 added console rake task 2010-11-24 13:46:11 +00:00
20280f2c5d switched to using bundle's own gemspec format
instead of jeweler
2010-11-24 13:46:03 +00:00
bf29696c46 Merge branch 'release/v0.0.3' into dev 2010-11-24 00:38:38 +00:00
92375b229a Merge branch 'release/v0.0.3' 2010-11-24 00:38:32 +00:00
e362c93d9a Version bump to 0.0.3 2010-11-24 00:38:19 +00:00
0f5b7449b0 updated readme 2010-11-24 00:37:28 +00:00
ea732b4734 updated gemfile and rakefile 2010-11-24 00:37:10 +00:00
62c3492c93 some whitespace cleanup 2010-11-24 00:36:44 +00:00
c5f52455cc replaced Redistat::Model#find alias to #fetch with
#lookup instead to avoid conflicts with
ActiveRecord
2010-11-24 00:36:25 +00:00
1226f8b89a Merge branch 'release/v0.0.2' into dev 2010-11-22 13:30:19 +00:00
bc7a563f20 Merge branch 'release/v0.0.2' 2010-11-22 13:30:14 +00:00
39da4c96a7 Version bump to 0.0.2 2010-11-22 13:29:56 +00:00
fa7903dc7a added gemspec to .gitignore 2010-11-22 13:29:41 +00:00
8a0e1a47a2 added proper support for hashed_label option, and
disabled it by default
2010-11-22 13:28:19 +00:00
8e3e54a027 updated gemspec description in Rakefile 2010-11-22 12:36:14 +00:00
07ae8b5c78 Merge branch 'release/v0.0.1' into dev 2010-11-22 12:31:45 +00:00
48 changed files with 2043 additions and 498 deletions

8
.gitignore vendored
View File

@@ -16,10 +16,12 @@ tmtags
## PROJECT::GENERAL
coverage
rdoc
pkg
pkg/*
*.gem
.bundle
Gemfile.lock
## PROJECT::SPECIFIC
.bundle/*
.yardoc/*
spec/db/*
doc
doc/*

1
.rvmrc Normal file
View File

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

12
Gemfile
View File

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

View File

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

View File

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

215
README.md
View File

@@ -1,15 +1,216 @@
# 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
# if using Redistat in multiple threads set this
# somewhere in the beginning of the execution stack
Redistat.thread_safe = true
## 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.
### Buffer ###
The buffer is a new, still semi-beta, feature aimed to reduce the number of Redis `hincrby` that Redistat sends. This should only really be useful when you're hitting north of 30,000 Redis requests per second, if your Redis server has limited resources, or against my recommendation you've opted to use 10, 20, or more label grouping levels.
Buffering tries to fold together multiple `store` calls into as few as possible by merging the statistics hashes from all calls and groups them based on scope, label, date depth, and more. You configure the the buffer by setting `Redistat.buffer_size` to an integer higher than 1. This basically tells Redistat how many `store` calls to buffer in memory before writing all data to Redis.
## 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 +221,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

View File

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

View File

@@ -1 +0,0 @@
0.0.1

View File

@@ -1,104 +1,101 @@
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 'monitor'
# 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/mixins/options'
require 'redistat/mixins/synchronize'
require 'redistat/mixins/database'
require 'redistat/mixins/date_helper'
require 'redistat/connection'
require 'redistat/buffer'
require 'redistat/collection'
require 'redistat/database'
require 'redistat/date'
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'
require 'redistat/core_ext/date'
require 'redistat/core_ext/time'
require 'redistat/core_ext/fixnum'
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 buffer
Buffer.instance
end
def buffer_size
buffer.size
end
def buffer_size=(size)
buffer.size = size
end
def thread_safe
Synchronize.thread_safe
end
def thread_safe=(value)
Synchronize.thread_safe = value
end
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
# ensure buffer is flushed on program exit
Kernel.at_exit do
Redistat.buffer.flush(true)
end

107
lib/redistat/buffer.rb Normal file
View File

@@ -0,0 +1,107 @@
require 'redistat/core_ext/hash'
module Redistat
class Buffer
include Synchronize
def self.instance
@instance ||= self.new
end
def size
synchronize do
@size ||= 0
end
end
def size=(value)
synchronize do
@size = value
end
end
def count
@count ||= 0
end
def store(key, stats, depth_limit, opts)
return false unless should_buffer?
to_flush = {}
buffkey = buffer_key(key, opts)
synchronize do
if !queue.has_key?(buffkey)
queue[buffkey] = { :key => key,
:stats => {},
:depth_limit => depth_limit,
:opts => opts }
end
queue[buffkey][:stats].merge_and_incr!(stats)
incr_count
# return items to be flushed if buffer size limit has been reached
to_flush = reset_queue
end
# flush any data that's been cleared from the queue
flush_data(to_flush)
true
end
def flush(force = false)
to_flush = {}
synchronize do
to_flush = reset_queue(force)
end
flush_data(to_flush)
end
private
# should always be called from within a synchronize block
def incr_count
@count ||= 0
@count += 1
end
def queue
@queue ||= {}
end
def should_buffer?
size > 1 # buffer size of 1 would be equal to not using buffer
end
# should always be called from within a synchronize block
def should_flush?
(!queue.blank? && count >= size)
end
# returns items to be flushed if buffer size limit has been reached
# should always be called from within a synchronize block
def reset_queue(force = false)
return {} if !force && !should_flush?
data = queue
@queue = {}
@count = 0
data
end
def flush_data(buffer_data)
buffer_data.each do |k, item|
Summary.update(item[:key], item[:stats], item[:depth_limit], item[:opts])
end
end
def buffer_key(key, opts)
# depth_limit is not needed as it's evident in key.to_s
opts_index = Summary.default_options.keys.sort { |a,b| a.to_s <=> b.to_s }.map do |k|
opts[k] if opts.has_key?(k)
end
"#{key.to_s}:#{opts_index.join(':')}"
end
end
end

View File

@@ -12,5 +12,9 @@ module Redistat
@depth = options[:depth] ||= nil
end
def total
@total ||= {}
end
end
end

View File

@@ -0,0 +1,84 @@
require 'monitor'
module Redistat
module Connection
REQUIRED_SERVER_VERSION = "1.3.10"
# TODO: Create a ConnectionPool instance object using Sychronize mixin to replace Connection class
class << self
# TODO: clean/remove all ref-less connections
def get(ref = nil)
ref ||= :default
synchronize do
connections[references[ref]] || create
end
end
def add(conn, ref = nil)
ref ||= :default
synchronize do
check_redis_version(conn)
references[ref] = conn.client.id
connections[conn.client.id] = conn
end
end
def create(options = {})
synchronize do
options = options.clone
ref = options.delete(:ref) || :default
options.reverse_merge!(default_options)
conn = (connections[connection_id(options)] ||= connection(options))
references[ref] = conn.client.id
conn
end
end
def connections
@connections ||= {}
end
def references
@references ||= {}
end
private
def monitor
@monitor ||= Monitor.new
end
def synchronize(&block)
monitor.synchronize(&block)
end
def connection(options)
check_redis_version(Redis.new(options))
end
def connection_id(options = {})
options = options.reverse_merge(default_options)
"redis://#{options[:host]}:#{options[:port]}/#{options[:db]}"
end
def check_redis_version(conn)
raise RedisServerIsTooOld if conn.info["redis_version"] < REQUIRED_SERVER_VERSION
conn
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
View 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'

View File

@@ -0,0 +1,8 @@
class Bignum
include Redistat::DateHelper
def to_time
Time.at(self)
end
end

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
require 'monitor'
module Redistat
module Synchronize
class << self
def included(base)
base.send(:include, InstanceMethods)
end
def monitor
@monitor ||= Monitor.new
end
def thread_safe
monitor.synchronize do
@thread_safe ||= false
end
end
def thread_safe=(value)
monitor.synchronize do
@thread_safe = value
end
end
end # << self
module InstanceMethods
def thread_safe
Synchronize.thread_safe
end
def thread_safe=(value)
Synchronize.thread_safe = value
end
def monitor
Synchronize.monitor
end
def synchronize(&block)
if thread_safe
monitor.synchronize(&block)
else
block.call
end
end
end # InstanceMethods
end
end

View File

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

View File

@@ -1,5 +1,7 @@
require 'active_support/core_ext/hash/indifferent_access'
module Redistat
class Result < ::ActiveSupport::HashWithIndifferentAccess
class Result < HashWithIndifferentAccess
attr_accessor :from
attr_accessor :till
@@ -12,12 +14,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

View File

@@ -2,23 +2,81 @@ module Redistat
class Summary
include Database
def self.update_all(key, stats = {}, depth_limit = nil)
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
class << self
def default_options
{ :enable_grouping => true,
:label_indexing => true,
:connection_ref => nil }
end
end
private
def self.update(key, stats, depth)
stats.each do |field, value|
db.hincrby key.to_s(depth), field, value
def buffer
Redistat.buffer
end
def update_all(key, stats = {}, depth_limit = nil, opts = {})
stats ||= {}
return if stats.empty?
options = default_options.merge((opts || {}).reject { |k,v| v.nil? })
depth_limit ||= key.depth
update_through_buffer(key, stats, depth_limit, options)
end
def update_through_buffer(*args)
update(*args) unless buffer.store(*args)
end
def update(key, stats, depth_limit, opts)
if opts[:enable_grouping]
stats = inject_group_summaries(stats)
key.groups.each do |k|
update_key(k, stats, depth_limit, opts[:connection_ref])
k.update_index if opts[:label_indexing]
end
else
update_key(key, stats, depth_limit, opts[:connection_ref])
end
end
private
def update_key(key, stats, depth_limit, connection_ref)
Date::DEPTHS.each do |depth|
update_fields(key, stats, depth, connection_ref)
break if depth == depth_limit
end
end
def update_fields(key, stats, depth, connection_ref = nil)
stats.each do |field, value|
db(connection_ref).hincrby key.to_s(depth), field, value
end
end
def 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 inject_group_summaries(stats)
inject_group_summaries!(stats.clone)
end
end
end
end

3
lib/redistat/version.rb Normal file
View File

@@ -0,0 +1,3 @@
module Redistat
VERSION = "0.3.0"
end

30
redistat.gemspec Normal file
View 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

View File

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

157
spec/buffer_spec.rb Normal file
View File

@@ -0,0 +1,157 @@
require "spec_helper"
describe Redistat::Buffer do
before(:each) do
@class = Redistat::Buffer
@buffer = Redistat::Buffer.instance
@key = mock('Key', :to_s => "Scope/label:2011")
@stats = {:count => 1, :views => 3}
@depth_limit = :hour
@opts = {:enable_grouping => true}
end
# let's cleanup after ourselves for the other specs
after(:each) do
@class.instance_variable_set("@instance", nil)
@buffer.size = 0
end
it "should provide instance of itself" do
@buffer.should be_a(@class)
end
it "should only buffer if buffer size setting is greater than 1" do
@buffer.size.should == 0
@buffer.send(:should_buffer?).should be_false
@buffer.size = 1
@buffer.size.should == 1
@buffer.send(:should_buffer?).should be_false
@buffer.size = 2
@buffer.size.should == 2
@buffer.send(:should_buffer?).should be_true
end
it "should only flush buffer if buffer size is greater than or equal to buffer size setting" do
@buffer.size.should == 0
@buffer.send(:queue).size.should == 0
@buffer.send(:should_flush?).should be_false
@buffer.send(:queue)[:hello] = 'world'
@buffer.send(:incr_count)
@buffer.send(:should_flush?).should be_true
@buffer.size = 5
@buffer.send(:should_flush?).should be_false
3.times { |i|
@buffer.send(:queue)[i] = i.to_s
@buffer.send(:incr_count)
}
@buffer.send(:should_flush?).should be_false
@buffer.send(:queue)[4] = '4'
@buffer.send(:incr_count)
@buffer.send(:should_flush?).should be_true
end
it "should force flush queue irregardless of result of #should_flush? when #reset_queue is called with true" do
@buffer.send(:queue)[:hello] = 'world'
@buffer.send(:incr_count)
@buffer.send(:should_flush?).should be_true
@buffer.size = 2
@buffer.send(:should_flush?).should be_false
@buffer.send(:reset_queue).should == {}
@buffer.instance_variable_get("@count").should == 1
@buffer.send(:reset_queue, true).should == {:hello => 'world'}
@buffer.instance_variable_get("@count").should == 0
end
it "should #flush_data into Summary.update properly" do
# the root level key value doesn't actually matter, but it's something like this...
data = {'ScopeName/label/goes/here:2011::true:true' => {
:key => @key,
:stats => @stats,
:depth_limit => @depth_limit,
:opts => @opts
}}
item = data.first[1]
Redistat::Summary.should_receive(:update).with(@key, @stats, @depth_limit, @opts)
@buffer.send(:flush_data, data)
end
it "should build #buffer_key correctly" do
opts = {:enable_grouping => true, :label_indexing => false, :connection_ref => nil}
@buffer.send(:buffer_key, @key, opts).should == "#{@key.to_s}::true:false"
opts = {:enable_grouping => false, :label_indexing => true, :connection_ref => :omg}
@buffer.send(:buffer_key, @key, opts).should == "#{@key.to_s}:omg:false:true"
end
describe "Buffering" do
it "should store items on buffer queue" do
@buffer.store(@key, @stats, @depth_limit, @opts).should be_false
@buffer.size = 5
@buffer.store(@key, @stats, @depth_limit, @opts).should be_true
@buffer.send(:queue).should have(1).item
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:count].should == 1
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:views].should == 3
@buffer.store(@key, @stats, @depth_limit, @opts).should be_true
@buffer.send(:queue).should have(1).items
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:count].should == 2
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:views].should == 6
end
it "should flush buffer queue when size is reached" do
key = mock('Key', :to_s => "Scope/labelx:2011")
@buffer.size = 10
Redistat::Summary.should_receive(:update).exactly(2).times.and_return do |k, stats, depth_limit, opts|
depth_limit.should == @depth_limit
opts.should == @opts
if k == @key
stats[:count].should == 6
stats[:views].should == 18
elsif k == key
stats[:count].should == 4
stats[:views].should == 12
end
end
6.times { @buffer.store(@key, @stats, @depth_limit, @opts).should be_true }
4.times { @buffer.store(key, @stats, @depth_limit, @opts).should be_true }
end
end
describe "Thread-Safety" do
it "should read/write to buffer queue in a thread-safe manner" do
# This spec passes wether thread safety is enabled or not. In short I need
# better specs for thread-safety, and personally a better understanding of
# thread-safety in general.
Redistat.thread_safe = true
key = mock('Key', :to_s => "Scope/labelx:2011")
@buffer.size = 100
Redistat::Summary.should_receive(:update).exactly(2).times.and_return do |k, stats, depth_limit, opts|
depth_limit.should == @depth_limit
opts.should == @opts
if k == @key
stats[:count].should == 60
stats[:views].should == 180
elsif k == key
stats[:count].should == 40
stats[:views].should == 120
end
end
threads = []
10.times do
threads << Thread.new {
6.times { @buffer.store(@key, @stats, @depth_limit, @opts).should be_true }
4.times { @buffer.store(key, @stats, @depth_limit, @opts).should be_true }
}
end
threads.each { |t| t.join }
end
it "should have better specs that actually fail when thread-safety is off"
end
end

View File

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

67
spec/connection_spec.rb Normal file
View File

@@ -0,0 +1,67 @@
require "spec_helper"
include Redistat
describe Redistat::Connection do
before(:each) do
@redis = Redistat.redis
end
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.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.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.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
# TODO: Test thread-safety
it "should be thread-safe" do
pending("need to figure out a way to test thread-safety")
end
end

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

View File

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

View File

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

View File

@@ -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,28 +99,109 @@ 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
def create_example_stats
key = Redistat::Key.new(@scope, @label, (first = Time.parse("2010-05-14 13:43")))
Redistat::Summary.update(key, @stats, :hour)
Redistat::Summary.send(:update_fields, 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"))
Redistat::Summary.update(key, @stats, :hour)
Redistat::Summary.send(:update_fields, key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 14:52"))
Redistat::Summary.send(:update_fields, key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02")))
Redistat::Summary.update(key, @stats, :hour)
Redistat::Summary.send(:update_fields, key, @stats, :hour)
[first - 1.hour, last + 1.hour]
end
end

View File

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

View File

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

View File

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

View File

@@ -5,56 +5,200 @@ 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
describe "Write Buffer" do
before(:each) do
Redistat.buffer_size = 20
end
after(:each) do
Redistat.buffer_size = 0
end
it "should buffer calls in memory before committing to Redis" do
14.times do
ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
end
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
5.times do
ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
end
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total["count"].should == 20
stats.total["weight"].should == 7390
end
it "should force flush buffer when #flush(true) is called" do
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
14.times do
ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
end
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
Redistat.buffer.flush(true)
stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
stats.total["count"].should == 14
stats.total["weight"].should == 6454
end
end
end

36
spec/options_spec.rb Normal file
View 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

View File

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

View File

@@ -13,19 +13,19 @@ describe Redistat::Summary do
end
it "should update a single summary properly" do
Redistat::Summary.update(@key, @stats, :hour)
Redistat::Summary.send(:update_fields, @key, @stats, :hour)
summary = db.hgetall(@key.to_s(:hour))
summary.should have(2).items
summary["views"].should == "3"
summary["visitors"].should == "2"
Redistat::Summary.update(@key, @stats, :hour)
Redistat::Summary.send(:update_fields, @key, @stats, :hour)
summary = db.hgetall(@key.to_s(:hour))
summary.should have(2).items
summary["views"].should == "6"
summary["visitors"].should == "4"
Redistat::Summary.update(@key, {"views" => -4, "visitors" => -3}, :hour)
Redistat::Summary.send(:update_fields, @key, {"views" => -4, "visitors" => -3}, :hour)
summary = db.hgetall(@key.to_s(:hour))
summary.should have(2).items
summary["views"].should == "2"
@@ -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.send(:update_fields, 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

64
spec/synchronize_spec.rb Normal file
View File

@@ -0,0 +1,64 @@
require "spec_helper"
describe Redistat::Synchronize do
it { should respond_to(:monitor) }
it { should respond_to(:thread_safe) }
it { should respond_to(:thread_safe=) }
describe "instanciated class with Redistat::Synchronize included" do
subject { SynchronizeSpecHelper.new }
it { should respond_to(:monitor) }
it { should respond_to(:thread_safe) }
it { should respond_to(:thread_safe=) }
it { should respond_to(:synchronize) }
end
describe "#synchronize method" do
before(:each) do
Redistat::Synchronize.instance_variable_set("@thread_safe", nil)
@obj = SynchronizeSpecHelper.new
end
it "should share single Monitor object across all objects" do
@obj.monitor.should == Redistat::Synchronize.monitor
end
it "should share thread_safe option across all objects" do
obj2 = SynchronizeSpecHelper.new
Redistat::Synchronize.thread_safe.should be_false
@obj.thread_safe.should be_false
obj2.thread_safe.should be_false
@obj.thread_safe = true
Redistat::Synchronize.thread_safe.should be_true
@obj.thread_safe.should be_true
obj2.thread_safe.should be_true
end
it "should not synchronize when thread_safe is disabled" do
# monitor receives :synchronize twice cause #thread_safe is _always_ synchronized
Redistat::Synchronize.monitor.should_receive(:synchronize).twice
@obj.thread_safe.should be_false # first #synchronize call
@obj.synchronize { 'foo' } # one #synchronize call while checking #thread_safe
end
it "should synchronize when thread_safe is enabled" do
Monitor.class_eval {
# we're stubbing synchronize to ensure it's being called correctly, but still need it :P
alias :real_synchronize :synchronize
}
Redistat::Synchronize.monitor.should_receive(:synchronize).with.exactly(4).times.and_return { |block|
Redistat::Synchronize.monitor.real_synchronize(&block)
}
@obj.thread_safe.should be_false # first synchronize call
Redistat::Synchronize.thread_safe = true # second synchronize call
@obj.synchronize { 'foo' } # two synchronize calls, once while checking thread_safe, once to call black
end
end
end
class SynchronizeSpecHelper
include Redistat::Synchronize
end

View File

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