von 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,58 +1,79 @@
1
1
  module Von
2
2
  class Period
3
- AVAILABLE_PERIODS = [ :hourly, :daily, :weekly, :monthly, :yearly ]
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 :counter_key
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(counter_key, period, length)
15
- @counter_key = counter_key
16
- @period = period
17
- @length = length
18
- @format = Von.config.send(:"#{@period}_format")
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 ||= case @period
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
- @period == :hourly
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
- # Returns the Redis hash key used for storing counts for this Period
44
- def hash_key
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
- # Returns the Redis list key used for storing current "active" counters
49
- def list_key
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
- # Returns the Redis field representation used for storing the count value
54
- def field
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
@@ -1,3 +1,3 @@
1
1
  module Von
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -18,24 +18,44 @@ describe Von::Config do
18
18
  @config.namespace.must_equal 'something'
19
19
  end
20
20
 
21
- it 'stores counter options per key and retrieves them' do
22
- options = { :monthly => 3, :total => false }
23
-
24
- @config.counter 'bar', options
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
- @config.namespace.must_equal 'von'
27
- @config.counter_options('bar').must_equal options
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 "allows config options to be updated via configure" do
31
- options = { :monthly => 3, :total => false }
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', options
49
+ config.counter 'bar', :current => :day
50
+ config.counter 'foo', :current => [ :month, :year ]
35
51
  end
36
52
 
37
- Von.config.namespace.must_equal 'von'
38
- Von.config.counter_options('bar').must_equal options
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
@@ -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
- mock_connection!
9
+ @redis = Redis.new
10
+ @redis.flushall
10
11
  end
11
12
 
12
- it "increments the total counter if given a single key" do
13
- Counter.increment('foo')
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 "increments the total counter for a key and it's parent keys" do
23
- Counter.increment('foo:bar')
24
-
25
- @store.has_key?('von:counters:foo').must_equal true
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
- it "increments a month counter" do
24
+
25
+ it "returns counts for a given period" do
36
26
  Von.configure do |config|
37
- config.counter 'foo', :monthly => 1
27
+ config.counter 'foo', :monthly => 2
38
28
  end
39
29
 
40
- Counter.increment('foo')
41
- Counter.increment('foo')
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
- @store.has_key?('von:counters:foo').must_equal true
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 "expires counters past the limit" do
39
+ it "returns best count for a given period" do
51
40
  Von.configure do |config|
52
- config.counter 'foo', :monthly => 1
41
+ config.counter 'foo', :best => [:minute, :week]
53
42
  end
54
43
 
55
- Counter.increment('foo')
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
- it "gets a total count for a counter" do
67
- Counter.increment('foo')
68
- Counter.increment('foo')
69
- Counter.increment('foo')
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
- Von.count('foo').must_equal 3
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 "gets a count for a time period and 0s missing entries" do
55
+ it "returns current count for a given period" do
75
56
  Von.configure do |config|
76
- config.counter 'foo', :monthly => 1, :hourly => 6
57
+ config.counter 'foo', :current => [:minute, :day]
77
58
  end
78
59
 
79
- Timecop.freeze(Time.local(2013, 02, 01, 05))
80
- Counter.increment('foo')
81
- Timecop.freeze(Time.local(2013, 02, 01, 07))
82
- Counter.increment('foo')
83
-
84
- Von.count('foo').must_equal 2
85
-
86
- Von.count('foo', :monthly).must_equal [{"2013-02" => 2}]
87
- Von.count('foo', :hourly).must_equal [
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