From e5b0aa32ed71151ddba5ca13326fd3b14172e6e5 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 18 Apr 2012 09:55:30 +0100 Subject: [PATCH 1/5] Add simplecov to tests --- redistat.gemspec | 1 + spec/spec_helper.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/redistat.gemspec b/redistat.gemspec index 7f958c8..b01b444 100644 --- a/redistat.gemspec +++ b/redistat.gemspec @@ -27,4 +27,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'rake', '>= 0.8.7' s.add_development_dependency 'rspec', '>= 2.1.0' s.add_development_dependency 'yard', '>= 0.6.3' + s.add_development_dependency 'simplecov', '>= 0.6.1' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 21210e4..5d751d7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,12 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +require 'simplecov' +SimpleCov.start do + add_filter '/spec' + add_filter '/vendor' +end + # require stuff require 'redistat' require 'rspec' From 9a084d28a04f077f004bab768e6411fe5fbf14af Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 18 Apr 2012 10:00:34 +0100 Subject: [PATCH 2/5] Attempt to fix unexplainably failing specs on travis-ci --- spec/synchronize_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/synchronize_spec.rb b/spec/synchronize_spec.rb index e859166..4f655ac 100644 --- a/spec/synchronize_spec.rb +++ b/spec/synchronize_spec.rb @@ -53,7 +53,10 @@ describe Redistat::Synchronize do } @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 + # two synchronize calls, once while checking thread_safe, once to call black + @obj.synchronize do + 'foo' + end end end From 4024887b58da42350bf75c3c47c408e1d2145070 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 18 Apr 2012 11:22:27 +0100 Subject: [PATCH 3/5] Small tweak to better deal with value caching --- lib/redistat/mixins/synchronize.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/redistat/mixins/synchronize.rb b/lib/redistat/mixins/synchronize.rb index 170485e..b421b03 100644 --- a/lib/redistat/mixins/synchronize.rb +++ b/lib/redistat/mixins/synchronize.rb @@ -14,7 +14,8 @@ module Redistat def thread_safe monitor.synchronize do - @thread_safe ||= false + return @thread_safe unless @thread_safe.nil? + @thread_safe = false end end From ce21b839f28e510ca1fbdf8caf8e2618a7de7338 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 18 Apr 2012 11:23:52 +0100 Subject: [PATCH 4/5] Rewrote Redistat::Sychronize rspecs Partly thanks to Travis-CI failing for an unexplained reason, but also cause they needed to be updated. --- spec/synchronize_spec.rb | 172 ++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 57 deletions(-) diff --git a/spec/synchronize_spec.rb b/spec/synchronize_spec.rb index 4f655ac..9e3833a 100644 --- a/spec/synchronize_spec.rb +++ b/spec/synchronize_spec.rb @@ -1,66 +1,124 @@ require "spec_helper" -describe Redistat::Synchronize do - it { should respond_to(:monitor) } - it { should respond_to(:thread_safe) } - it { should respond_to(:thread_safe=) } +module Redistat + describe Synchronize do - 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) } + let(:klass) { 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 - # two synchronize calls, once while checking thread_safe, once to call black - @obj.synchronize do - 'foo' + describe '.included' do + it 'includes InstanceMethods in passed object' do + base = mock('Base') + base.should_receive(:include).with(klass::InstanceMethods) + klass.included(base) end - end - end + end # included -end + describe '.monitor' do + it 'returns a Monitor instance' do + klass.monitor.should be_a(Monitor) + end + + it 'caches Monitor instance' do + klass.monitor.object_id.should == klass.monitor.object_id + end + end # monitor + + describe '.thread_safe' do + after { klass.instance_variable_set('@thread_safe', nil) } + + it 'returns value of @thread_safe' do + klass.instance_variable_set('@thread_safe', true) + klass.thread_safe.should be_true + end + + it 'defaults to false' do + klass.thread_safe.should be_false + end + + it 'uses #synchronize' do + klass.monitor.should_receive(:synchronize).once + klass.thread_safe.should be_nil + end + end # thread_safe + + describe '.thread_safe=' do + after { klass.instance_variable_set('@thread_safe', nil) } + + it 'sets @thread_safe' do + klass.instance_variable_get('@thread_safe').should be_nil + klass.thread_safe = true + klass.instance_variable_get('@thread_safe').should be_true + end + + it 'uses #synchronize' do + klass.monitor.should_receive(:synchronize).once + klass.thread_safe = true + klass.instance_variable_get('@thread_safe').should be_nil + end + end # thread_safe= + + describe "InstanceMethods" do + subject { SynchronizeSpecHelper.new } + + describe '.monitor' do + it 'defers to Redistat::Synchronize' do + klass.should_receive(:monitor).once + subject.monitor + end + end # monitor + + describe '.thread_safe' do + it ' defers to Redistat::Synchronize' do + klass.should_receive(:thread_safe).once + subject.thread_safe + end + end # thread_safe + + describe '.thread_safe=' do + it 'defers to Redistat::Synchronize' do + klass.should_receive(:thread_safe=).once.with(true) + subject.thread_safe = true + end + end # thread_safe= + + describe 'when #thread_safe is true' do + before { subject.stub(:thread_safe).and_return(true) } + + describe '.synchronize' do + it 'defers to #monitor' do + subject.monitor.should_receive(:synchronize).once + subject.synchronize { 'foo' } + end + + it 'passes block along to #monitor.synchronize' do + yielded = false + subject.synchronize { yielded = true } + yielded.should be_true + end + end # synchronize + end # when #thread_safe is true + + describe 'when #thread_safe is false' do + before { subject.stub(:thread_safe).and_return(false) } + + describe '.synchronize' do + it 'does not defer to #monitor' do + subject.monitor.should_not_receive(:synchronize) + subject.synchronize { 'foo' } + end + + it 'yields block' do + yielded = false + subject.synchronize { yielded = true } + yielded.should be_true + end + end # synchronize + end # when #thread_safe is false + + end + + end # Synchronize +end # Redistat class SynchronizeSpecHelper include Redistat::Synchronize From ea68a91a58f1e44cc086114a363758e7e01b399e Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Wed, 18 Apr 2012 11:33:39 +0100 Subject: [PATCH 5/5] Update, and add travis-ci status to readme --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ac00f09..2d6d829 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Redistat # +# Redistat [![Build Status](https://secure.travis-ci.org/jimeh/redistat.png)](http://travis-ci.org/jimeh/redistat) A Redis-backed statistics storage and querying library written in Ruby. @@ -15,14 +15,14 @@ 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 ## +## 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) ## +## Usage (Crash Course) view\_stats.rb: @@ -39,7 +39,7 @@ Redistat.thread_safe = true ``` -### Simple Example ### +### Simple Example Store: @@ -60,7 +60,7 @@ ViewStats.find('hello', 3.hour.ago, 1.hour.from_now).total ``` -### Advanced Example ### +### Advanced Example Store page view on product #44 from Chrome 11: @@ -131,16 +131,16 @@ finder.children.map { |child| child.total } ``` -## Terminology ## +## Terminology -### Scope ### +### 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 ### +### Label Identifier string to separate different types and groups of statistics from each other. The first argument of the `#store`, `#find`, and `#fetch` methods @@ -156,7 +156,7 @@ 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 ### +### 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 @@ -166,7 +166,7 @@ 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) ### +### 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`, @@ -176,7 +176,7 @@ 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 ### +### 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 @@ -189,7 +189,7 @@ key for the 13th hour. If both 13:00 and 14:00 was returned, you would get results from two whole hours. Hence if you want up to the second data, use an end time of `1.hour.from_now`. -### The Finder Object ### +### 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 @@ -203,9 +203,9 @@ comes to the finder. -## Internals ## +## Internals -### Storing / Writing ### +### 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: @@ -258,7 +258,7 @@ 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 ### +### 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 @@ -271,7 +271,7 @@ 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 ### +### 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 @@ -287,21 +287,21 @@ Redistat how many `store` calls to buffer in memory before writing all data to Redis. -## Todo ## +## Todo * More details in Readme. * Documentation. * Anything else that becomes apparent after real-world use. -## Credits ## +## 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 ## +## Note on Patches/Pull Requests * Fork the project. * Make your feature addition or bug fix. @@ -313,7 +313,7 @@ company time to further develop the project. * Send me a pull request. Bonus points for topic branches. -## License and Copyright ## +## License and Copyright Copyright (c) 2011 Jim Myhrberg.