von 0.1.0 → 0.2.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 +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
|