tally_counter 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +17 -9
- data/lib/tally_counter/version.rb +1 -1
- data/lib/tally_counter.rb +20 -4
- data/test/tally_counter_test.rb +33 -16
- metadata +5 -5
data/README.md
CHANGED
@@ -59,14 +59,27 @@ to cause a count skip.
|
|
59
59
|
## Keys and Scoring
|
60
60
|
|
61
61
|
The system uses Redis sorted sets for tracking application hits.
|
62
|
-
For each request, a key will be
|
62
|
+
For each request, a key will be generated, and the score for the
|
63
63
|
remote request ip will be incremented by 1.
|
64
64
|
|
65
65
|
Keys are generated like 'tally_counter:1371283200' where the time
|
66
66
|
is the epoch seconds floor for the current window. The floor for
|
67
|
-
a 5 minute interval at 12:38 would be 12:35, at 12:33
|
67
|
+
a 5 minute interval at 12:38 would be 12:35, at 12:33 it's 12:30,
|
68
68
|
and so on.
|
69
69
|
|
70
|
+
Keys are generated using the `TallyCounter::KeyGenerate` class.
|
71
|
+
This can be used in client applications for generating keys for
|
72
|
+
Redis lookups.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
# Create a key generator for 5 minute windows with :foo namespace
|
76
|
+
key_generate = TallyCounter::KeyGenerate.new(300, :foo)
|
77
|
+
# Generate a key for the current time
|
78
|
+
key_generate.for(Time.now)
|
79
|
+
# Generate a key from 10 minutes ago
|
80
|
+
key_generate.for(Time.now, 2)
|
81
|
+
```
|
82
|
+
|
70
83
|
It is recommended to use a scheduled process to inspect tally_counter
|
71
84
|
sets past a certain age (say, 3 days) and prune them to keep your
|
72
85
|
data set small and clean.
|
@@ -75,21 +88,16 @@ Finding totals for a range of time can be accomplished via a Redis
|
|
75
88
|
zunionstore on a range of keys. For example, if you have a a 5
|
76
89
|
minute interval and want to see the last 15 minutes, simple grab
|
77
90
|
the current window and the 2 previous and union them with equally
|
78
|
-
weighted scoring. See `TallyCounter::Window#
|
91
|
+
weighted scoring. See `TallyCounter::Window#floor` for generating
|
79
92
|
window times and offsets.
|
80
93
|
|
81
94
|
## Reporting
|
82
95
|
|
83
96
|
In the interest of giving this gem a single responsibility, reporting
|
84
97
|
can be offloaded to other systems. It should be easy to deploy
|
85
|
-
a separate admin application
|
98
|
+
a separate admin application connected to the same server, and use
|
86
99
|
the `TallyCounter::Window` class for generating keys.
|
87
100
|
|
88
|
-
## Todo
|
89
|
-
|
90
|
-
Extract key generation to a utility for use by middleware and client
|
91
|
-
apps.
|
92
|
-
|
93
101
|
## Contributing
|
94
102
|
|
95
103
|
1. Fork it
|
data/lib/tally_counter.rb
CHANGED
@@ -14,7 +14,7 @@ module TallyCounter
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# Returns the floor time for a given window. For example,
|
17
|
-
# if the interval is 5 minutes and it is 12:38, the
|
17
|
+
# if the interval is 5 minutes and it is 12:38, the floor
|
18
18
|
# would be 12:35. If offset is 1, the floor would be 12:30
|
19
19
|
#
|
20
20
|
# @param [Time] a time instance, commonly Time.now
|
@@ -26,6 +26,22 @@ module TallyCounter
|
|
26
26
|
|
27
27
|
end
|
28
28
|
|
29
|
+
class KeyGenerate
|
30
|
+
# @param [Integer] a seconds interval for Window use
|
31
|
+
# @param [String] optional app namespace
|
32
|
+
def initialize(interval, namespace = nil)
|
33
|
+
@window = TallyCounter::Window.new(interval)
|
34
|
+
@namespace = namespace
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [Time] time for a key, often Time.now
|
38
|
+
# @param [Integer] offset for window to step back
|
39
|
+
def for(time, offset = 0)
|
40
|
+
timestamp = @window.floor(time, offset).to_i
|
41
|
+
[@namespace, "tally_counter", timestamp].compact.join(':')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
29
45
|
class Middleware
|
30
46
|
|
31
47
|
# @param app - a rack compliant app
|
@@ -68,11 +84,11 @@ module TallyCounter
|
|
68
84
|
end
|
69
85
|
|
70
86
|
def current_key
|
71
|
-
|
87
|
+
key_generate.for(Time.now)
|
72
88
|
end
|
73
89
|
|
74
|
-
def
|
75
|
-
TallyCounter::
|
90
|
+
def key_generate
|
91
|
+
@key_generate ||= TallyCounter::KeyGenerate.new(@interval, @namespace)
|
76
92
|
end
|
77
93
|
|
78
94
|
end
|
data/test/tally_counter_test.rb
CHANGED
@@ -2,17 +2,15 @@ require "test/unit"
|
|
2
2
|
require "rack/test"
|
3
3
|
require File.expand_path('../../lib/tally_counter', __FILE__)
|
4
4
|
|
5
|
-
|
6
|
-
def test(name, &block)
|
5
|
+
class TallyCounterTestCase < Test::Unit::TestCase
|
6
|
+
def self.test(name, &block)
|
7
7
|
name = name.gsub(/\W+/, ' ').strip
|
8
8
|
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
|
9
9
|
define_method(test_name, &block)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
class TallyCounterWindowTest <
|
14
|
-
extend TestHelper
|
15
|
-
|
13
|
+
class TallyCounterWindowTest < TallyCounterTestCase
|
16
14
|
test "initializing with interval seconds" do
|
17
15
|
window = TallyCounter::Window.new(60)
|
18
16
|
assert_equal 60, window.interval
|
@@ -38,8 +36,30 @@ class TallyCounterWindowTest < Test::Unit::TestCase
|
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
41
|
-
class
|
42
|
-
|
39
|
+
class TallyCounterKeyGenerateTest < TallyCounterTestCase
|
40
|
+
def time
|
41
|
+
Time.parse('2013-06-15 08:52:41 -0700')
|
42
|
+
end
|
43
|
+
|
44
|
+
test "generating a key without a namespace" do
|
45
|
+
key_generate = TallyCounter::KeyGenerate.new(300)
|
46
|
+
assert_equal 'tally_counter:1371311400', key_generate.for(time)
|
47
|
+
end
|
48
|
+
|
49
|
+
test "generating a key with a namespace" do
|
50
|
+
key_generate = TallyCounter::KeyGenerate.new(300, :some_app)
|
51
|
+
assert_equal 'some_app:tally_counter:1371311400', key_generate.for(time)
|
52
|
+
end
|
53
|
+
|
54
|
+
test "generating a key for a given window offset" do
|
55
|
+
key_generate = TallyCounter::KeyGenerate.new(300, :some_app)
|
56
|
+
assert_equal 'some_app:tally_counter:1371311400', key_generate.for(time, 0)
|
57
|
+
assert_equal 'some_app:tally_counter:1371311100', key_generate.for(time, 1)
|
58
|
+
assert_equal 'some_app:tally_counter:1371310800', key_generate.for(time, 2)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class TallyCounterMiddlewareTest < TallyCounterTestCase
|
43
63
|
include Rack::Test::Methods
|
44
64
|
|
45
65
|
def redis
|
@@ -104,10 +124,9 @@ class TallyCounterMiddlewareTest < Test::Unit::TestCase
|
|
104
124
|
end
|
105
125
|
|
106
126
|
test "tolerating a timeout error" do
|
107
|
-
fake_redis = Object.new
|
108
|
-
|
109
|
-
|
110
|
-
end
|
127
|
+
fake_redis = Object.new
|
128
|
+
def fake_redis.zincrby(*)
|
129
|
+
sleep 5
|
111
130
|
end
|
112
131
|
self.app_options = {:redis => fake_redis}
|
113
132
|
|
@@ -116,15 +135,13 @@ class TallyCounterMiddlewareTest < Test::Unit::TestCase
|
|
116
135
|
end
|
117
136
|
|
118
137
|
test "tolerating a redis error" do
|
119
|
-
fake_redis = Object.new
|
120
|
-
|
121
|
-
|
122
|
-
end
|
138
|
+
fake_redis = Object.new
|
139
|
+
def fake_redis.zincrby(*)
|
140
|
+
raise Redis::ConnectionError
|
123
141
|
end
|
124
142
|
self.app_options = {:redis => fake_redis}
|
125
143
|
|
126
144
|
assert_nothing_raised { get "/" }
|
127
145
|
assert_match "ok", last_response.body
|
128
146
|
end
|
129
|
-
|
130
147
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tally_counter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2013-06-15 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement: &
|
16
|
+
requirement: &2160507200 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2160507200
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rack-test
|
27
|
-
requirement: &
|
27
|
+
requirement: &2160506760 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2160506760
|
36
36
|
description: Tally web application hits with Rack & Redis sorted sets
|
37
37
|
email:
|
38
38
|
- xternal1+github@gmail.com
|