visit-counter-omri 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in visit-counter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Tom Caspy
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ # VisitCounter
2
+
3
+ [![Build Status](https://secure.travis-ci.org/KensoDev/visit-counter.png)](https://secure.travis-ci.org/KensoDev/visit-counter)
4
+
5
+ VisitCounter is a gem which solves the annoying problem of counting visits and displaying them in real time. In an SQL database, for a site with a lot of hits, this can cause quite a lot of overhead. VisitCounter aims to solve this by using a quick key-value store to keep a delta, and only persist to the SQL DB when the delta crosses a certain percent of the saved counter.
6
+ It can be used transparently, by overriding the accessor to the counter, or simply by using the helper functions it defines - incr_counter, read_counter, get_counter_delta and nullify_counter.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'visit-counter'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install visit-counter
21
+
22
+ ## Usage
23
+
24
+ a. the default storage engine is redis. If you have a global $redis for your redis connection, we default to using that. Otherwise, or if you want to specify a different connection, in an initializer you should define it by:
25
+
26
+ VisitCounter::Store::RedisStore.redis = Redis.new(host: "your_redis_host", port: port)
27
+
28
+ b. in the class you wish to have a visit counter simply declare
29
+ include VisitCounter
30
+ from this moment on, you can use the incr_counter(:counter_name), nullify_counter(:counter_name) and read_counter(:counter_name) methods
31
+ You can also do something like this:
32
+
33
+ class Foo < ActiveRecord::Base
34
+ include VisitCounter
35
+ cached_counter :counter_name
36
+ end
37
+
38
+ this will override the counter_name method to read the live counter (from both database and the NoSQL storage) and add a increase_counter_name method for upping the counter by 1 (in the NoSQL and/or persist to DB when needed)
39
+
40
+ ##Thresholds
41
+
42
+ the default behaviour of visit counter is that once the visits pass 30% of the staged number, the visit counter stages the changes and nullifies the delta. You can, however, tweak that method. including VisitCounter in a class creates two class attribute_accessors, one named visit_counter_threshold_method, the other visit_counter_threshold.
43
+ visit_counter_threshold_method accepts either :static or :percent (the default), the threshold is either the decimal percent (0.1 for 10%) or an integer for :static. in case of the :static method you will percist to the DB once every (threshold) times the counter goes up.
44
+
45
+ so you might want to do something like that:
46
+
47
+ class Foo < ActiveRecord::Base
48
+ include VisitCounter
49
+ cached_counter :counter_name
50
+ self.visit_counter_threshold_method = :static
51
+ self.visit_counter_threshold = 100
52
+ end
53
+
54
+ if you want the counter to persist to database once every 100 views.
55
+
56
+ if you need to run callbacks for whatever reason after you update the counter just:
57
+
58
+ class Foo < ActiveRecord::Base
59
+ include VisitCounter
60
+ cached_counter :counter_name
61
+ self.update_callback_method = :update_something
62
+ end
63
+
64
+ ## Contributing
65
+
66
+ 1. Fork it
67
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
68
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
69
+ 4. Push to the branch (`git push origin my-new-feature`)
70
+ 5. Create new Pull Request
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ require "visit-counter/version"
2
+ require "visit-counter/key"
3
+ require "visit-counter/visit_counter"
4
+ require "visit-counter/store/abstract_store"
5
+ require "visit-counter/store/redis_store"
6
+ require "visit-counter/store/rails_store"
7
+ require "visit-counter/store"
@@ -0,0 +1,7 @@
1
+ module VisitCounter
2
+ class Key
3
+ def self.key(object, method)
4
+ "visit_counter::#{object.class.to_s}::#{object.id}::#{method}"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module VisitCounter
2
+ class Store
3
+ @@engine ||= VisitCounter::Store::RedisStore
4
+
5
+ class << self
6
+
7
+ def set_engine(store)
8
+ @@engine = store
9
+ end
10
+
11
+ def engine
12
+ @@engine
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module VisitCounter
2
+ class Store
3
+ class AbstractStore
4
+ class << self
5
+ #placeholders
6
+
7
+ def incr(key)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def get(key)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def nullify(key)
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ module VisitCounter
2
+ class Store
3
+ class RailsStore < VisitCounter::Store::AbstractStore
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,66 @@
1
+ require "redis"
2
+
3
+ module VisitCounter
4
+ class Store
5
+ class RedisStore < VisitCounter::Store::AbstractStore
6
+ @@redis = nil
7
+ class << self
8
+
9
+ def redis=(r)
10
+ if r.is_a?(Redis)
11
+ @@redis = r
12
+ else
13
+ @@redis = Redis.new(r)
14
+ end
15
+ end
16
+
17
+ def redis
18
+ if @@redis.nil? && defined?($redis)
19
+ @@redis = $redis
20
+ else
21
+ @@redis
22
+ end
23
+ end
24
+
25
+ def incr(key)
26
+ redis.incr(key).to_i
27
+ end
28
+
29
+ def get(key)
30
+ redis.get(key).to_i
31
+ end
32
+
33
+ def nullify(key)
34
+ redis.set(key, 0)
35
+ end
36
+
37
+ def substract(key, by)
38
+ redis.decrby(key, by)
39
+ end
40
+
41
+ def acquire_lock(object)
42
+ redis.setnx(lock_key(object), 1)
43
+ end
44
+
45
+ def unlock!(object)
46
+ redis.del(lock_key(object))
47
+ end
48
+
49
+ def lock_key(object)
50
+ "#{object.class.name.downcase}_#{object.id}_object_cache_lock"
51
+ end
52
+
53
+ def with_lock(object, &block)
54
+ if acquire_lock(object)
55
+ begin
56
+ yield
57
+ ensure
58
+ unlock!(object)
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module VisitCounter
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,94 @@
1
+ module VisitCounter
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ class << self
6
+ #defining class instance attributes
7
+ attr_accessor :visit_counter_threshold, :visit_counter_threshold_method, :update_callback_method
8
+ end
9
+ end
10
+ base.send(:include, InstanceMethods)
11
+ base.send(:extend, ClassMethods)
12
+ end
13
+
14
+ module InstanceMethods
15
+ def incr_counter(name)
16
+ key = VisitCounter::Key.key(self, name)
17
+ count = VisitCounter::Store.engine.incr(key)
18
+ staged_count = self.send(:read_attribute, name).to_i
19
+ if Helper.passed_limit?(self, staged_count, count, name)
20
+ Helper.persist(self, staged_count, count, name)
21
+ end
22
+ end
23
+
24
+ def get_counter_delta(name)
25
+ key = VisitCounter::Key.key(self, name)
26
+ VisitCounter::Store.engine.get(key)
27
+ end
28
+
29
+ def read_counter(name)
30
+ current_count = self.send(:read_attribute, name).to_i
31
+ count = get_counter_delta(name)
32
+
33
+ current_count + count
34
+ end
35
+
36
+ def nullify_counter_cache(name, substract = nil)
37
+ key = VisitCounter::Key.key(self, name)
38
+ if substract
39
+ VisitCounter::Store.engine.substract(key, substract)
40
+ else
41
+ VisitCounter::Store.engine.nullify(key)
42
+ end
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+
48
+ def cached_counter(name)
49
+
50
+ self.send(:define_method, name) do
51
+ read_counter(name)
52
+ end
53
+
54
+ self.send(:define_method, "increase_#{name}") do
55
+ incr_counter(name)
56
+ end
57
+ end
58
+ end
59
+
60
+ class Helper
61
+ class << self
62
+ def passed_limit?(object, staged_count, diff, name)
63
+ method = object.class.visit_counter_threshold_method || :percent
64
+ threshold = object.class.visit_counter_threshold || default_threshold(method)
65
+
66
+ if method.to_sym == :static
67
+ diff >= threshold
68
+ elsif method.to_sym == :percent
69
+ return true if staged_count.to_i == 0
70
+ diff.to_f / staged_count.to_f >= threshold
71
+ end
72
+ end
73
+
74
+ def persist(object, staged_count, diff, name)
75
+ VisitCounter::Store.engine.with_lock(object) do
76
+ object.update_attribute(name, staged_count + diff)
77
+ if object.class.update_callback_method
78
+ object.send(object.class.update_callback_method)
79
+ end
80
+ object.nullify_counter_cache(name, diff)
81
+ end
82
+ end
83
+
84
+ def default_threshold(method)
85
+ if method.to_sym == :static
86
+ 10
87
+ elsif method.to_sym == :percent
88
+ 0.3
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+
3
+ describe VisitCounter::Store do
4
+
5
+ describe "engine" do
6
+ it "should have the redis store by default" do
7
+ VisitCounter::Store.engine.should == VisitCounter::Store::RedisStore
8
+ end
9
+
10
+ it "should be able to change the storage engine" do
11
+ VisitCounter::Store.set_engine(VisitCounter::Store::RailsStore)
12
+ VisitCounter::Store.engine.should == VisitCounter::Store::RailsStore
13
+
14
+ #switching back
15
+ VisitCounter::Store.set_engine(VisitCounter::Store::RedisStore)
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,175 @@
1
+ require "spec_helper"
2
+
3
+ class DummyObject
4
+ include VisitCounter
5
+
6
+ attr_accessor :counter, :update_callback_method
7
+
8
+ def update_attribute(attribute, value)
9
+ self.send("#{attribute}=", value)
10
+ end
11
+
12
+ def update_something
13
+ nil
14
+ end
15
+
16
+ def read_attribute(name)
17
+ #yeah, evals are evil, but it works and it's for testing purposes only. we assume read_attribute is defined the same as in AR wherever we include this module
18
+ eval("@#{name}")
19
+ end
20
+ end
21
+
22
+ VisitCounter::Store::RedisStore.redis = Redis.new(host: "localhost")
23
+ VisitCounter::Store::RedisStore.redis.flushdb
24
+
25
+ describe VisitCounter do
26
+ describe "incrementing counters" do
27
+ before :each do
28
+ @d = DummyObject.new
29
+ @d.stub!(:id).and_return(1)
30
+ @d.nullify_counter_cache(:counter)
31
+ end
32
+
33
+ it "should increase the counter from nil / zero using the incr_counter method" do
34
+ @d.counter.should be_nil
35
+ @d.incr_counter(:counter)
36
+ @d.counter.should == 1
37
+ end
38
+
39
+ it "should not increase the counter if not passing the threshold" do
40
+ @d.counter = 100
41
+ @d.incr_counter(:counter)
42
+ @d.counter.should == 100
43
+ 30.times do
44
+ @d.incr_counter(:counter)
45
+ end
46
+ @d.counter.should == 130
47
+
48
+ #should still be 143, because of the percentage thingie
49
+ 30.times do
50
+ @d.incr_counter(:counter)
51
+ end
52
+ @d.counter.should == 130
53
+ end
54
+ end
55
+
56
+ describe "reading counters" do
57
+
58
+ before :each do
59
+ @d = DummyObject.new
60
+ @d.stub!(:id).and_return(1)
61
+ @d.nullify_counter_cache(:counter)
62
+ end
63
+
64
+ it "should allow to read an unchanged counter" do
65
+ @d.counter = 10
66
+ @d.read_counter(:counter).should == 10
67
+ end
68
+
69
+ it "should read an updated counter" do
70
+ @d.counter = 10
71
+ @d.incr_counter(:counter)
72
+ @d.read_counter(:counter).should == 11
73
+ end
74
+ end
75
+
76
+ describe "static threshold" do
77
+ before :each do
78
+ @d = DummyObject.new
79
+ @d.stub!(:id).and_return(1)
80
+ @d.nullify_counter_cache(:counter)
81
+ DummyObject.visit_counter_threshold_method = :static
82
+ end
83
+
84
+ it "should persist after setting a static threshold of 1" do
85
+ DummyObject.visit_counter_threshold = 1
86
+ @d.counter = 10
87
+ @d.incr_counter(:counter)
88
+ @d.counter.should == 11
89
+ end
90
+
91
+ it "should not persist after setting a static threshold of 2" do
92
+ DummyObject.visit_counter_threshold = 2
93
+ @d.counter = 10
94
+ @d.incr_counter(:counter)
95
+ @d.counter.should == 10
96
+
97
+ @d.incr_counter(:counter)
98
+ @d.counter.should == 12
99
+ end
100
+ end
101
+
102
+ describe "percent threshold" do
103
+ before :each do
104
+ @d = DummyObject.new
105
+ @d.stub!(:id).and_return(1)
106
+ @d.nullify_counter_cache(:counter)
107
+ DummyObject.visit_counter_threshold_method = :percent
108
+ end
109
+
110
+ it "should persist when passing a percent" do
111
+ DummyObject.visit_counter_threshold = 0.1
112
+ @d.counter = 10
113
+ @d.incr_counter(:counter)
114
+ @d.counter.should == 11
115
+ end
116
+ end
117
+
118
+ describe "locked objects" do
119
+ before :each do
120
+ @d = DummyObject.new
121
+ @d.stub!(:id).and_return(1)
122
+ @d.nullify_counter_cache(:counter)
123
+ end
124
+
125
+ it "should lock object when updating" do
126
+ VisitCounter::Store.engine.should_receive(:with_lock)
127
+ VisitCounter::Helper.persist(@d, 1, 1, :counter)
128
+ end
129
+
130
+ it "should not persist if object is locked" do
131
+ VisitCounter::Store.engine.stub!(:acquire_lock).and_return(false)
132
+ @d.should_not_receive(:update_attribute)
133
+ VisitCounter::Helper.persist(@d, 1, 1, :counter)
134
+ end
135
+
136
+ it "should persist if object is locked" do
137
+ VisitCounter::Store.engine.stub!(:acquire_lock).and_return(true)
138
+ @d.should_receive(:update_attribute)
139
+ VisitCounter::Helper.persist(@d, 1, 1, :counter)
140
+ end
141
+ end
142
+
143
+ describe "overriding the getter" do
144
+ before :all do
145
+ DummyObject.cached_counter :counter
146
+ end
147
+
148
+ before :each do
149
+ @d = DummyObject.new
150
+ @d.stub!(:id).and_return(1)
151
+ @d.nullify_counter_cache(:counter)
152
+ end
153
+
154
+ it "should define the methods" do
155
+ @d.should respond_to(:increase_counter)
156
+ end
157
+
158
+ it "should set the counter" do
159
+ @d.counter = 10
160
+ @d.increase_counter
161
+ @d.counter.should == 11
162
+ end
163
+ end
164
+
165
+ describe "persist with callbacks" do
166
+ it "should use update_something method" do
167
+ @d = DummyObject.new
168
+ @d.class.update_callback_method = :update_something
169
+ @d.stub!(:id).and_return(1)
170
+ @d.should_receive(:update_something)
171
+ @d.incr_counter :counter
172
+ end
173
+ end
174
+
175
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require "visit-counter"
3
+
4
+ RSpec.configure do |config|
5
+ # config.mock_with :mocha
6
+ # config.mock_with :flexmock
7
+ # config.mock_with :rr
8
+ config.mock_with :rspec
9
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/visit-counter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Tom Caspy"]
6
+ gem.email = ["tcaspy@gmail.com"]
7
+ gem.description = %q{Simple counter increment which only writes to DB once in a while}
8
+ gem.summary = %q{No need to write to db each visit, save the visits to a quick DB like redis or memcached, and write to the SQL db once reads exeeded a certain threshold}
9
+ gem.homepage = "https://github.com/KensoDev/visit-counter"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "visit-counter-omri"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = VisitCounter::VERSION
17
+
18
+ gem.add_development_dependency(%q<rspec>, [">= 0"])
19
+ gem.add_development_dependency(%q<rake>, ["~> 0.9.2"])
20
+ gem.add_dependency(%q<redis>, [">= 0"])
21
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: visit-counter-omri
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tom Caspy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ prerelease: false
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ none: false
23
+ type: :development
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ! '>='
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ none: false
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ prerelease: false
33
+ requirement: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.9.2
38
+ none: false
39
+ type: :development
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 0.9.2
45
+ none: false
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis
48
+ prerelease: false
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ none: false
55
+ type: :runtime
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ none: false
62
+ description: Simple counter increment which only writes to DB once in a while
63
+ email:
64
+ - tcaspy@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - lib/visit-counter.rb
75
+ - lib/visit-counter/key.rb
76
+ - lib/visit-counter/store.rb
77
+ - lib/visit-counter/store/abstract_store.rb
78
+ - lib/visit-counter/store/rails_store.rb
79
+ - lib/visit-counter/store/redis_store.rb
80
+ - lib/visit-counter/version.rb
81
+ - lib/visit-counter/visit_counter.rb
82
+ - spec/lib/store_spec.rb
83
+ - spec/lib/visit_counter_spec.rb
84
+ - spec/spec_helper.rb
85
+ - visit-counter.gemspec
86
+ homepage: https://github.com/KensoDev/visit-counter
87
+ licenses: []
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ none: false
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ none: false
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 1.8.24
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: No need to write to db each visit, save the visits to a quick DB like redis
110
+ or memcached, and write to the SQL db once reads exeeded a certain threshold
111
+ test_files:
112
+ - spec/lib/store_spec.rb
113
+ - spec/lib/visit_counter_spec.rb
114
+ - spec/spec_helper.rb