von 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +46 -5
- data/Rakefile +1 -1
- data/lib/von.rb +42 -5
- data/lib/von/config.rb +54 -24
- data/lib/von/counter.rb +41 -83
- data/lib/von/counters/best.rb +68 -0
- data/lib/von/counters/commands.rb +41 -0
- data/lib/von/counters/current.rb +43 -0
- data/lib/von/counters/period.rb +62 -0
- data/lib/von/counters/total.rb +49 -0
- data/lib/von/period.rb +51 -30
- data/lib/von/version.rb +1 -1
- data/test/config_test.rb +31 -11
- data/test/counter_test.rb +40 -65
- data/test/counters/best_test.rb +53 -0
- data/test/counters/current_test.rb +55 -0
- data/test/counters/period_test.rb +78 -0
- data/test/counters/total_test.rb +33 -0
- data/test/period_test.rb +62 -40
- data/test/test_helper.rb +2 -62
- data/test/von_test.rb +25 -3
- data/von.gemspec +1 -0
- metadata +31 -3
- data/lib/von/model_counter.rb +0 -28
@@ -0,0 +1,41 @@
|
|
1
|
+
module Von
|
2
|
+
module Counters
|
3
|
+
module Commands
|
4
|
+
def hget(*args)
|
5
|
+
Von.connection.hget(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def hgetall(*args)
|
9
|
+
Von.connection.hgetall(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def hincrby(*args)
|
13
|
+
Von.connection.hincrby(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def hset(*args)
|
17
|
+
Von.connection.hset(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def lrange(*args)
|
21
|
+
Von.connection.lrange(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def rpush(*args)
|
25
|
+
Von.connection.rpush(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def llen(*args)
|
29
|
+
Von.connection.llen(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def lpop(*args)
|
33
|
+
Von.connection.lpop(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def hdel(*args)
|
37
|
+
Von.connection.hdel(*args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Von
|
2
|
+
module Counters
|
3
|
+
class Current
|
4
|
+
include Von::Counters::Commands
|
5
|
+
|
6
|
+
# Initialize a new Counter
|
7
|
+
#
|
8
|
+
# field - counter field name
|
9
|
+
def initialize(field, periods = nil)
|
10
|
+
@field = field.to_sym
|
11
|
+
@periods = periods || []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the Redis hash key used for storing counts for this Counter
|
15
|
+
def hash_key
|
16
|
+
"#{Von.config.namespace}:counters:currents:#{@field}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_timestamp(time_unit)
|
20
|
+
hget("#{hash_key}:#{time_unit}", 'timestamp')
|
21
|
+
end
|
22
|
+
|
23
|
+
def increment
|
24
|
+
return if @periods.empty?
|
25
|
+
|
26
|
+
@periods.each do |period|
|
27
|
+
if period.timestamp != current_timestamp(period.time_unit)
|
28
|
+
hset("#{hash_key}:#{period.time_unit}", 'total', 1)
|
29
|
+
hset("#{hash_key}:#{period.time_unit}", 'timestamp', period.timestamp)
|
30
|
+
else
|
31
|
+
hincrby("#{hash_key}:#{period.time_unit}", 'total', 1)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def count(time_unit)
|
37
|
+
count = hget("#{hash_key}:#{time_unit}", 'total')
|
38
|
+
count.nil? ? 0 : count.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Von
|
2
|
+
module Counters
|
3
|
+
class Period
|
4
|
+
include Von::Counters::Commands
|
5
|
+
|
6
|
+
def initialize(field, periods = nil)
|
7
|
+
@field = field.to_sym
|
8
|
+
@periods = periods || []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the Redis hash key used for storing counts for this Period
|
12
|
+
def hash_key(time_unit)
|
13
|
+
"#{Von.config.namespace}:counters:#{@field}:#{time_unit}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the Redis list key used for storing current "active" counters
|
17
|
+
def list_key(time_unit)
|
18
|
+
"#{Von.config.namespace}:lists:#{@field}:#{time_unit}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def increment
|
22
|
+
return if @periods.empty?
|
23
|
+
|
24
|
+
@periods.each do |period|
|
25
|
+
_hash_key = hash_key(period.time_unit)
|
26
|
+
_list_key = list_key(period.time_unit)
|
27
|
+
|
28
|
+
hincrby(_hash_key, period.timestamp, 1)
|
29
|
+
|
30
|
+
unless lrange(_list_key, 0, -1).include?(period.timestamp)
|
31
|
+
rpush(_list_key, period.timestamp)
|
32
|
+
end
|
33
|
+
|
34
|
+
if llen(_list_key) > period.length
|
35
|
+
expired_counter = lpop(_list_key)
|
36
|
+
hdel(_hash_key, expired_counter)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Count the fields for the given time period for this Counter.
|
42
|
+
#
|
43
|
+
# Returns an Array of Hashes representing the count
|
44
|
+
def count(time_unit)
|
45
|
+
return if @periods.empty?
|
46
|
+
|
47
|
+
counts = []
|
48
|
+
this_period = nil
|
49
|
+
_period = @periods.select { |p| p.time_unit == time_unit }.first
|
50
|
+
|
51
|
+
_period.length.times do |i|
|
52
|
+
this_period = _period.prev(i)
|
53
|
+
counts.unshift(this_period)
|
54
|
+
end
|
55
|
+
|
56
|
+
keys = hgetall(hash_key(time_unit))
|
57
|
+
counts.map { |date| { :timestamp => date, :count => keys.fetch(date, 0).to_i }}
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Von
|
2
|
+
module Counters
|
3
|
+
class Total
|
4
|
+
include Von::Counters::Commands
|
5
|
+
attr_reader :field
|
6
|
+
|
7
|
+
# Initialize a new Counter
|
8
|
+
#
|
9
|
+
# field - counter field name
|
10
|
+
def initialize(field)
|
11
|
+
@field = field.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the Redis hash key used for storing counts for this Counter
|
15
|
+
def hash_key
|
16
|
+
"#{Von.config.namespace}:counters:#{@field}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Increment the total count for this Counter
|
20
|
+
# If the key has time periods specified, increment those.
|
21
|
+
#
|
22
|
+
# Returns the Integer total for the key
|
23
|
+
def increment
|
24
|
+
hincrby(hash_key, 'total', 1).to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
# Count the "total" field for this Counter.
|
28
|
+
#
|
29
|
+
# Returns an Integer count
|
30
|
+
def count
|
31
|
+
count = hget(hash_key, 'total')
|
32
|
+
count.nil? ? 0 : count.to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
# Lookup the count for this Counter in Redis.
|
36
|
+
# If a Period argument is given we lookup the count for
|
37
|
+
# all of the possible units (not expired), zeroing ones that
|
38
|
+
# aren't set in Redis already.
|
39
|
+
#
|
40
|
+
# period - A Period to lookup
|
41
|
+
#
|
42
|
+
# Returns an Integer representing the count or an Array of counts.
|
43
|
+
def self.count(field)
|
44
|
+
Counter.new(field).count
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/von/period.rb
CHANGED
@@ -1,58 +1,79 @@
|
|
1
1
|
module Von
|
2
2
|
class Period
|
3
|
-
|
3
|
+
PERIOD_MAPPING = {
|
4
|
+
:minutely => :minute,
|
5
|
+
:hourly => :hour,
|
6
|
+
:daily => :day,
|
7
|
+
:weekly => :week,
|
8
|
+
:monthly => :month,
|
9
|
+
:yearly => :year
|
10
|
+
}
|
11
|
+
AVAILABLE_PERIODS = PERIOD_MAPPING.keys
|
12
|
+
AVAILABLE_TIME_UNITS = PERIOD_MAPPING.values
|
4
13
|
|
5
|
-
attr_reader :
|
14
|
+
attr_reader :name
|
6
15
|
attr_reader :length
|
7
16
|
attr_reader :format
|
8
17
|
|
9
18
|
# Initialize a Period object
|
10
19
|
#
|
11
|
-
# counter - the field name for the counter
|
12
20
|
# period - the time period one of AVAILABLE_PERIODS
|
13
21
|
# length - length of period
|
14
|
-
def initialize(
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
def initialize(period, length = nil)
|
23
|
+
name = period.to_sym
|
24
|
+
if AVAILABLE_PERIODS.include?(name)
|
25
|
+
@name = name
|
26
|
+
elsif AVAILABLE_TIME_UNITS.include?(name)
|
27
|
+
@name = PERIOD_MAPPING.invert[name]
|
28
|
+
else
|
29
|
+
raise ArgumentError, "`#{period}' is not a valid period"
|
30
|
+
end
|
31
|
+
@length = length
|
32
|
+
@format = Von.config.send(:"#{@name}_format")
|
19
33
|
end
|
20
34
|
|
21
35
|
# Returns a Symbol representing the time unit
|
22
36
|
# for the current period.
|
23
37
|
def time_unit
|
24
|
-
@time_unit ||=
|
25
|
-
when :hourly
|
26
|
-
:hour
|
27
|
-
when :daily
|
28
|
-
:day
|
29
|
-
when :weekly
|
30
|
-
:week
|
31
|
-
when :monthly
|
32
|
-
:month
|
33
|
-
when :yearly
|
34
|
-
:year
|
35
|
-
end
|
38
|
+
@time_unit ||= PERIOD_MAPPING[@name]
|
36
39
|
end
|
37
40
|
|
38
41
|
# Returns True or False if the period is hourly
|
39
42
|
def hours?
|
40
|
-
@
|
43
|
+
@name == :hourly
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns True or False if the period is minutely
|
47
|
+
def minutes?
|
48
|
+
@name == :minutely
|
49
|
+
end
|
50
|
+
|
51
|
+
def beginning(time)
|
52
|
+
if minutes?
|
53
|
+
time.change(:seconds => 0)
|
54
|
+
else
|
55
|
+
time.send(:"beginning_of_#{time_unit}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def prev(unit = 1)
|
60
|
+
beginning(unit.send(time_unit.to_sym).ago).strftime(@format)
|
61
|
+
end
|
62
|
+
|
63
|
+
def timestamp
|
64
|
+
beginning(Time.now).strftime(format)
|
41
65
|
end
|
42
66
|
|
43
|
-
|
44
|
-
|
45
|
-
@hash ||= "#{Von.config.namespace}:counters:#{@counter_key}:#{@period}"
|
67
|
+
def self.unit_to_period(time_unit)
|
68
|
+
PERIOD_MAPPING.invert[time_unit]
|
46
69
|
end
|
47
70
|
|
48
|
-
|
49
|
-
|
50
|
-
@list ||= "#{Von.config.namespace}:lists:#{@counter_key}:#{@period}"
|
71
|
+
def self.exists?(period)
|
72
|
+
AVAILABLE_PERIODS.include?(period)
|
51
73
|
end
|
52
74
|
|
53
|
-
|
54
|
-
|
55
|
-
Time.now.strftime(format)
|
75
|
+
def self.time_unit_exists?(time_unit)
|
76
|
+
AVAILABLE_TIME_UNITS.include?(time_unit)
|
56
77
|
end
|
57
78
|
end
|
58
79
|
end
|
data/lib/von/version.rb
CHANGED
data/test/config_test.rb
CHANGED
@@ -18,24 +18,44 @@ describe Von::Config do
|
|
18
18
|
@config.namespace.must_equal 'something'
|
19
19
|
end
|
20
20
|
|
21
|
-
it
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
it "sets periods via counter method" do
|
22
|
+
Von.configure do |config|
|
23
|
+
config.counter 'bar', :monthly => 3, :daily => 6
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
Von.config.periods[:bar].length.must_equal 2
|
27
|
+
Von.config.periods[:bar].first.name.must_equal :monthly
|
28
|
+
Von.config.periods[:bar].first.length.must_equal 3
|
29
|
+
Von.config.periods[:bar].last.name.must_equal :daily
|
30
|
+
Von.config.periods[:bar].last.length.must_equal 6
|
28
31
|
end
|
29
32
|
|
30
|
-
it "
|
31
|
-
|
33
|
+
it "sets bests via counter method" do
|
34
|
+
Von.configure do |config|
|
35
|
+
config.counter 'bar', :best => :day
|
36
|
+
config.counter 'foo', :best => [ :month, :year ]
|
37
|
+
end
|
38
|
+
|
39
|
+
Von.config.bests[:bar].first.must_be_instance_of Von::Period
|
40
|
+
Von.config.bests[:bar].first.name.must_equal :daily
|
41
|
+
Von.config.bests[:foo].first.must_be_instance_of Von::Period
|
42
|
+
Von.config.bests[:foo].first.name.must_equal :monthly
|
43
|
+
Von.config.bests[:foo].last.must_be_instance_of Von::Period
|
44
|
+
Von.config.bests[:foo].last.name.must_equal :yearly
|
45
|
+
end
|
32
46
|
|
47
|
+
it "sets currents via counter method" do
|
33
48
|
Von.configure do |config|
|
34
|
-
config.counter 'bar',
|
49
|
+
config.counter 'bar', :current => :day
|
50
|
+
config.counter 'foo', :current => [ :month, :year ]
|
35
51
|
end
|
36
52
|
|
37
|
-
Von.config.
|
38
|
-
Von.config.
|
53
|
+
Von.config.currents[:bar].first.must_be_instance_of Von::Period
|
54
|
+
Von.config.currents[:bar].first.name.must_equal :daily
|
55
|
+
Von.config.currents[:foo].first.must_be_instance_of Von::Period
|
56
|
+
Von.config.currents[:foo].first.name.must_equal :monthly
|
57
|
+
Von.config.currents[:foo].last.must_be_instance_of Von::Period
|
58
|
+
Von.config.currents[:foo].last.name.must_equal :yearly
|
39
59
|
end
|
40
60
|
|
41
61
|
end
|
data/test/counter_test.rb
CHANGED
@@ -4,94 +4,69 @@ describe Von::Counter do
|
|
4
4
|
Counter = Von::Counter
|
5
5
|
|
6
6
|
before :each do
|
7
|
-
Timecop.freeze(Time.local(2013, 01))
|
7
|
+
Timecop.freeze(Time.local(2013, 01, 01, 01, 01))
|
8
8
|
Von.config.init!
|
9
|
-
|
9
|
+
@redis = Redis.new
|
10
|
+
@redis.flushall
|
10
11
|
end
|
11
12
|
|
12
|
-
it "
|
13
|
-
|
14
|
-
|
15
|
-
@store.has_key?('von:counters:foo').must_equal true
|
16
|
-
@store['von:counters:foo']['total'].must_equal 1
|
17
|
-
|
18
|
-
Counter.increment('foo')
|
19
|
-
@store['von:counters:foo']['total'].must_equal 2
|
13
|
+
it "returns count for key" do
|
14
|
+
3.times { Von.increment('foo') }
|
15
|
+
Counter.new('foo').total.must_equal 3
|
20
16
|
end
|
21
17
|
|
22
|
-
it "
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@store['von:counters:foo']['total'].must_equal 1
|
27
|
-
@store.has_key?('von:counters:foo:bar').must_equal true
|
28
|
-
@store['von:counters:foo:bar']['total'].must_equal 1
|
29
|
-
|
30
|
-
Counter.increment('foo:bar')
|
31
|
-
@store['von:counters:foo']['total'].must_equal 2
|
32
|
-
@store['von:counters:foo:bar']['total'].must_equal 2
|
18
|
+
it "returns count for key and parent keys" do
|
19
|
+
3.times { Von.increment('foo:bar') }
|
20
|
+
Counter.new('foo').total.must_equal 3
|
21
|
+
Counter.new('foo:bar').total.must_equal 3
|
33
22
|
end
|
34
23
|
|
35
|
-
|
24
|
+
|
25
|
+
it "returns counts for a given period" do
|
36
26
|
Von.configure do |config|
|
37
|
-
config.counter 'foo', :monthly =>
|
27
|
+
config.counter 'foo', :monthly => 2
|
38
28
|
end
|
39
29
|
|
40
|
-
|
41
|
-
|
30
|
+
Von.increment('foo')
|
31
|
+
Timecop.freeze(Time.local(2013, 02))
|
32
|
+
Von.increment('foo')
|
33
|
+
Timecop.freeze(Time.local(2013, 03))
|
34
|
+
Von.increment('foo')
|
42
35
|
|
43
|
-
|
44
|
-
@store.has_key?('von:counters:foo:monthly').must_equal true
|
45
|
-
@store['von:counters:foo']['total'].must_equal 2
|
46
|
-
@store['von:counters:foo:monthly']['2013-01'].must_equal 2
|
47
|
-
@store['von:lists:foo:monthly'].size.must_equal 1
|
36
|
+
Counter.new('foo').per(:month).must_equal [{:timestamp => "2013-02", :count => 1}, {:timestamp => "2013-03", :count => 1}]
|
48
37
|
end
|
49
38
|
|
50
|
-
it "
|
39
|
+
it "returns best count for a given period" do
|
51
40
|
Von.configure do |config|
|
52
|
-
config.counter 'foo', :
|
41
|
+
config.counter 'foo', :best => [:minute, :week]
|
53
42
|
end
|
54
43
|
|
55
|
-
|
56
|
-
Timecop.freeze(Time.local(2013, 02))
|
57
|
-
Counter.increment('foo')
|
58
|
-
|
59
|
-
@store.has_key?('von:counters:foo').must_equal true
|
60
|
-
@store.has_key?('von:counters:foo:monthly').must_equal true
|
61
|
-
@store['von:counters:foo']['total'].must_equal 2
|
62
|
-
@store['von:counters:foo:monthly'].has_key?('2013-02').must_equal true
|
63
|
-
@store['von:lists:foo:monthly'].size.must_equal 1
|
64
|
-
end
|
44
|
+
Von.increment('foo')
|
65
45
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
46
|
+
Timecop.freeze(Time.local(2013, 01, 13, 06, 05))
|
47
|
+
4.times { Von.increment('foo') }
|
48
|
+
Timecop.freeze(Time.local(2013, 01, 20, 06, 10))
|
49
|
+
3.times { Von.increment('foo') }
|
70
50
|
|
71
|
-
|
51
|
+
Counter.new('foo').best(:minute).must_equal({:timestamp => "2013-01-13 06:05", :count => 4})
|
52
|
+
Counter.new('foo').best(:week).must_equal({:timestamp => "2013-01-07", :count => 4})
|
72
53
|
end
|
73
54
|
|
74
|
-
it "
|
55
|
+
it "returns current count for a given period" do
|
75
56
|
Von.configure do |config|
|
76
|
-
config.counter 'foo', :
|
57
|
+
config.counter 'foo', :current => [:minute, :day]
|
77
58
|
end
|
78
59
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
{"2013-02-01 02:00"=>0},
|
89
|
-
{"2013-02-01 03:00"=>0},
|
90
|
-
{"2013-02-01 04:00"=>0},
|
91
|
-
{"2013-02-01 05:00"=>1},
|
92
|
-
{"2013-02-01 06:00"=>0},
|
93
|
-
{"2013-02-01 07:00"=>1}
|
94
|
-
]
|
60
|
+
4.times { Von.increment('foo') }
|
61
|
+
Timecop.freeze(Time.local(2013, 01, 20, 06, 10))
|
62
|
+
3.times { Von.increment('foo') }
|
63
|
+
|
64
|
+
Counter.new('foo').this(:minute).must_equal 3
|
65
|
+
Counter.new('foo').current(:minute).must_equal 3
|
66
|
+
Counter.new('foo').this(:day).must_equal 3
|
67
|
+
Counter.new('foo').current(:day).must_equal 3
|
68
|
+
Counter.new('foo').today.must_equal 3
|
95
69
|
end
|
96
70
|
|
71
|
+
|
97
72
|
end
|