visit-counter 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +18 -3
- data/lib/visit-counter/store/abstract_store.rb +21 -0
- data/lib/visit-counter/store/rails_store.rb +6 -0
- data/lib/visit-counter/store/redis_store.rb +40 -0
- data/lib/visit-counter/version.rb +1 -1
- data/lib/visit-counter/visit_counter.rb +46 -10
- data/spec/lib/visit_counter_spec.rb +49 -1
- data/visit-counter.gemspec +1 -1
- metadata +28 -10
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
#
|
1
|
+
# VisitCounter
|
2
2
|
|
3
|
-
|
3
|
+
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.
|
4
|
+
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.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
@@ -18,7 +19,21 @@ Or install it yourself as:
|
|
18
19
|
|
19
20
|
## Usage
|
20
21
|
|
21
|
-
|
22
|
+
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:
|
23
|
+
|
24
|
+
VisitCounter::Store::RedisStore.redis = Redis.new(host: "your_redis_host", port: port)
|
25
|
+
|
26
|
+
b. in the class you wish to have a visit counter simply declare
|
27
|
+
include VisitCounter
|
28
|
+
from this moment on, you can use the incr_counter(:counter_name), nullify_counter(:counter_name) and read_counter(:counter_name) methods
|
29
|
+
You can also do something like this:
|
30
|
+
|
31
|
+
class Foo < ActiveRecord::Base
|
32
|
+
include VisitCounter
|
33
|
+
cached_counter :counter_name
|
34
|
+
end
|
35
|
+
|
36
|
+
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)
|
22
37
|
|
23
38
|
## Contributing
|
24
39
|
|
@@ -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,40 @@
|
|
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
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,16 +1,52 @@
|
|
1
1
|
module VisitCounter
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:include, InstanceMethods)
|
5
|
+
base.send(:extend, ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def incr_counter(name)
|
10
|
+
key = VisitCounter::Key.key(self, name)
|
11
|
+
count = VisitCounter::Store.engine.incr(key)
|
12
|
+
current_count = self.send(:read_attribute, name).to_i
|
13
|
+
if current_count / (current_count + count).to_f < 0.7
|
14
|
+
self.update_attribute(name, current_count + count)
|
15
|
+
nullify_counter_cache(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_counter_delta(name)
|
20
|
+
key = VisitCounter::Key.key(self, name)
|
21
|
+
VisitCounter::Store.engine.get(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def read_counter(name)
|
25
|
+
current_count = self.send(:read_attribute, name).to_i
|
26
|
+
count = get_counter_delta(name)
|
27
|
+
|
28
|
+
current_count + count
|
29
|
+
end
|
30
|
+
|
31
|
+
def nullify_counter_cache(name)
|
32
|
+
key = VisitCounter::Key.key(self, name)
|
33
|
+
VisitCounter::Store.engine.nullify(key)
|
9
34
|
end
|
10
35
|
end
|
11
36
|
|
12
|
-
|
13
|
-
|
14
|
-
|
37
|
+
module ClassMethods
|
38
|
+
|
39
|
+
def cached_counter(name)
|
40
|
+
self.send(:alias_method, "real_#{name}", name)
|
41
|
+
|
42
|
+
self.send(:define_method, name) do
|
43
|
+
read_counter(name)
|
44
|
+
end
|
45
|
+
|
46
|
+
self.send(:define_method, "increase_#{name}") do
|
47
|
+
incr_counter(name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
15
51
|
end
|
16
52
|
end
|
@@ -8,9 +8,14 @@ class DummyObject
|
|
8
8
|
def update_attribute(attribute, value)
|
9
9
|
self.send("#{attribute}=", value)
|
10
10
|
end
|
11
|
+
|
12
|
+
def read_attribute(name)
|
13
|
+
#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
|
14
|
+
eval("@#{name}")
|
15
|
+
end
|
11
16
|
end
|
12
17
|
|
13
|
-
VisitCounter::Store::RedisStore.redis =
|
18
|
+
VisitCounter::Store::RedisStore.redis = Redis.new(host: "localhost")
|
14
19
|
|
15
20
|
describe VisitCounter do
|
16
21
|
describe "incrementing counters" do
|
@@ -41,6 +46,49 @@ describe VisitCounter do
|
|
41
46
|
end
|
42
47
|
@d.counter.should == 143
|
43
48
|
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "reading counters" do
|
52
|
+
|
53
|
+
before :each do
|
54
|
+
@d = DummyObject.new
|
55
|
+
@d.stub!(:id).and_return(1)
|
56
|
+
@d.nullify_counter_cache(:counter)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should allow to read an unchanged counter" do
|
60
|
+
@d.counter = 10
|
61
|
+
@d.read_counter(:counter).should == 10
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should read an updated counter" do
|
65
|
+
@d.counter = 10
|
66
|
+
@d.incr_counter(:counter)
|
67
|
+
@d.read_counter(:counter).should == 11
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "overriding the getter" do
|
72
|
+
before :all do
|
73
|
+
DummyObject.cached_counter :counter
|
74
|
+
end
|
75
|
+
|
76
|
+
before :each do
|
77
|
+
@d = DummyObject.new
|
78
|
+
@d.stub!(:id).and_return(1)
|
79
|
+
@d.nullify_counter_cache(:counter)
|
80
|
+
end
|
44
81
|
|
82
|
+
it "should define the methods" do
|
83
|
+
@d.should respond_to(:real_counter)
|
84
|
+
@d.should respond_to(:increase_counter)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should set the counter" do
|
88
|
+
@d.counter = 10
|
89
|
+
@d.increase_counter
|
90
|
+
@d.counter.should == 11
|
91
|
+
end
|
45
92
|
end
|
93
|
+
|
46
94
|
end
|
data/visit-counter.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |gem|
|
|
6
6
|
gem.email = ["tcaspy@gmail.com"]
|
7
7
|
gem.description = %q{Simple counter increment which only writes to DB once in a while}
|
8
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 = ""
|
9
|
+
gem.homepage = "https://github.com/KensoDev/visit-counter"
|
10
10
|
|
11
11
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
12
|
gem.files = `git ls-files`.split("\n")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: visit-counter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-08-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: rake
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ~>
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: 0.9.2
|
33
38
|
type: :development
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.9.2
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: redis
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,7 +53,12 @@ dependencies:
|
|
43
53
|
version: '0'
|
44
54
|
type: :runtime
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
47
62
|
description: Simple counter increment which only writes to DB once in a while
|
48
63
|
email:
|
49
64
|
- tcaspy@gmail.com
|
@@ -59,13 +74,16 @@ files:
|
|
59
74
|
- lib/visit-counter.rb
|
60
75
|
- lib/visit-counter/key.rb
|
61
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
|
62
80
|
- lib/visit-counter/version.rb
|
63
81
|
- lib/visit-counter/visit_counter.rb
|
64
82
|
- spec/lib/store_spec.rb
|
65
83
|
- spec/lib/visit_counter_spec.rb
|
66
84
|
- spec/spec_helper.rb
|
67
85
|
- visit-counter.gemspec
|
68
|
-
homepage:
|
86
|
+
homepage: https://github.com/KensoDev/visit-counter
|
69
87
|
licenses: []
|
70
88
|
post_install_message:
|
71
89
|
rdoc_options: []
|
@@ -85,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
103
|
version: '0'
|
86
104
|
requirements: []
|
87
105
|
rubyforge_project:
|
88
|
-
rubygems_version: 1.8.
|
106
|
+
rubygems_version: 1.8.24
|
89
107
|
signing_key:
|
90
108
|
specification_version: 3
|
91
109
|
summary: No need to write to db each visit, save the visits to a quick DB like redis
|