tally_counter 0.0.1 → 0.1.0
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 +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
|