event-counter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +120 -0
- data/Rakefile +1 -0
- data/config/database.ci.yml +5 -0
- data/event-counter.gemspec +29 -0
- data/lib/event-counter.rb +1 -0
- data/lib/event_counter/active_record_extension.rb +224 -0
- data/lib/event_counter/version.rb +4 -0
- data/lib/event_counter.rb +148 -0
- data/spec/lib/event_counter_spec.rb +346 -0
- data/spec/lib/performance_spec.rb +55 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/support/fabrications.rb +2 -0
- data/spec/support/matchers.rb +31 -0
- metadata +165 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5c824074d24fd8d6d102c55c7cd81c4026e3cdd0
|
4
|
+
data.tar.gz: 906bf98133f1f082d6a3f1c1d4831c64f7974ea3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8db9c66d38029ccf99b8fa828f5936a1dc8ea84412822d7fc1037b4a2d4979988f429e4b2a96379d23e821b4ad08e792409f617b7ac1eb28f71f429341a608fc
|
7
|
+
data.tar.gz: a4085a8f4901cd3854b50ff56791c17bcd345d4269845e4ab5523a3bc35c65ced9bd88e6702ac2c0e2c77f8882edc73afdc5a244fa5f8a2bb927866b0f4c1678
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Anton Orel
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
EventCounter
|
2
|
+
===============
|
3
|
+
|
4
|
+
EventCounter is a database based event counter with throttling per time intervals.
|
5
|
+
|
6
|
+
Usage
|
7
|
+
-----
|
8
|
+
|
9
|
+
Let's define counters in model
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Article < ActiveRecord::Base
|
13
|
+
has_counter :views, interval: 5.minutes
|
14
|
+
|
15
|
+
# :year, :month, :week and :day symbols are supported
|
16
|
+
has_counter :views_by_week, interval: :week
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
Let's count...
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
article = Article.create!
|
24
|
+
|
25
|
+
article.up!(:views)
|
26
|
+
# creates counter (if it doesn't exist) with value 1 and on Time.now() rounded to 5 minutes, e.x.:
|
27
|
+
#=> #<EventCounter id: 1, name: "views", value: 1, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:20:00">
|
28
|
+
|
29
|
+
# at once
|
30
|
+
article.up!(:views, 3)
|
31
|
+
#=> #<EventCounter id: 1, name: "views", value: 4, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:20:00">
|
32
|
+
# it will update counter (if the other exists in that interval) with value 3 and on Time.now() rounded to 5 minutes
|
33
|
+
|
34
|
+
# later
|
35
|
+
article.up!(:views, 5)
|
36
|
+
#=> #<EventCounter id: 2, name: "views", value: 5, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:25:00">
|
37
|
+
article.down!(:views, 2)
|
38
|
+
#=> #<EventCounter id: 2, name: "views", value: 3, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:25:00">
|
39
|
+
|
40
|
+
# anytime or in a background job
|
41
|
+
article.up!(:views, 7, on_time: 10.minutes.ago)
|
42
|
+
#=> #<EventCounter id: 3, name: "views", value: 7, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:15:00">
|
43
|
+
|
44
|
+
# we have not got? let's fix it
|
45
|
+
article.up!(:views, 9, on_time: 10.minutes.ago, force: true)
|
46
|
+
#=> #<EventCounter id: 3, name: "views", value: 9, countable_id: 1, countable_type: "Article", created_at: "2014-10-16 23:15:00">
|
47
|
+
```
|
48
|
+
|
49
|
+
Let's get some statistics for our charts...
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
article.data_for(:views)
|
53
|
+
#=> [[2014-10-16 23:15:00 +0400, 9], [2014-10-16 23:20:00 +0400, 4], [2014-10-16 23:25:00 +0400, 3]]
|
54
|
+
|
55
|
+
article.data_for(:views, interval: 10.minutes)
|
56
|
+
#=> [[2014-10-16 23:10:00 +0400, 9], [2014-10-16 23:20:00 +0400, 7]]
|
57
|
+
|
58
|
+
range = Time.mktime(2014, 10, 16, 23, 0)..Time.mktime(2014, 10, 16, 23, 10)
|
59
|
+
article.data_for(:views, interval: 10.minutes, range: range)
|
60
|
+
#=> [[2014-10-16 23:00:00 +0400, 0], [2014-10-16 23:10:00 +0400, 9]]
|
61
|
+
|
62
|
+
article.data_for(:views, interval: :day)
|
63
|
+
#=> [[2014-10-16 00:00:00 +0400, 16]]
|
64
|
+
|
65
|
+
article.data_for(:views, interval: :day, raw: true)
|
66
|
+
#=> [{"created_at" => "2014-10-16 00:00:00+04", "value" => "16"}]
|
67
|
+
# raw result will make difference in performance on a big data
|
68
|
+
|
69
|
+
# class wide
|
70
|
+
range = Time.mktime(2014, 10, 15)..Time.mktime(2014, 10, 16)
|
71
|
+
Article.data_for(:views, interval: :day, range: range)
|
72
|
+
#=> [[2014-10-15 00:00:00 +0400, 0], [2014-10-16 00:00:00 +0400, 16]]
|
73
|
+
```
|
74
|
+
|
75
|
+
Limitations
|
76
|
+
-----------
|
77
|
+
|
78
|
+
- It works *ONLY* with *PostgreSQL* at the moment.
|
79
|
+
- ActiveRecord 3+
|
80
|
+
- It's polymorphic association.
|
81
|
+
- Use it in production with caution because it's early release.
|
82
|
+
|
83
|
+
|
84
|
+
Installation
|
85
|
+
--------------
|
86
|
+
|
87
|
+
Add gem to Gemfile
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
gem 'event_counter'
|
91
|
+
```
|
92
|
+
|
93
|
+
Create migration `rails g migration create_event_counters` with the
|
94
|
+
following code:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class CreateEventCounters < ActiveRecord::Migration
|
98
|
+
|
99
|
+
def change
|
100
|
+
create_table :event_counters, force: true do |t|
|
101
|
+
t.string :name, null: false
|
102
|
+
t.integer :value, default: 0, null: false
|
103
|
+
t.references :countable, polymorphic: true, null: false
|
104
|
+
|
105
|
+
t.datetime :created_at
|
106
|
+
end
|
107
|
+
|
108
|
+
add_index :event_counters, :countable_type
|
109
|
+
add_index :event_counters,
|
110
|
+
[:countable_type, :name, :countable_id], name: 'complex_index'
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
License
|
118
|
+
----
|
119
|
+
|
120
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'event_counter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'event-counter'
|
8
|
+
spec.version = EventCounterVersion::VERSION
|
9
|
+
spec.authors = ['Anton Orel']
|
10
|
+
spec.email = ['eagle.anton@gmail.com']
|
11
|
+
spec.summary = 'Event counter with throttling per time interval'
|
12
|
+
spec.description = 'Database based event counter with throttling per time intervals'
|
13
|
+
spec.homepage = 'https://github.com/skyeagle/event-counter'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'activerecord', '~> 3'
|
22
|
+
spec.add_dependency 'pg', '~> 0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
+
spec.add_development_dependency 'database_cleaner', '~> 0'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
27
|
+
spec.add_development_dependency 'fabrication', '~> 0'
|
28
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'event_counter'
|
@@ -0,0 +1,224 @@
|
|
1
|
+
class EventCounter < ActiveRecord::Base
|
2
|
+
# This module adds functionality to ActiveRecord
|
3
|
+
module ActiveRecordExtension
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :event_counters
|
8
|
+
self.event_counters = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# :nodoc:
|
12
|
+
module ClassMethods
|
13
|
+
def event_counter_for(name, interval)
|
14
|
+
event_counters[name] = interval
|
15
|
+
|
16
|
+
clause = { name: name.to_s }
|
17
|
+
|
18
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
19
|
+
has_many name,
|
20
|
+
-> { where(clause) }, as: :countable, class_name: 'EventCounter'
|
21
|
+
else
|
22
|
+
has_many name,
|
23
|
+
conditions: clause, as: :countable, class_name: 'EventCounter'
|
24
|
+
end
|
25
|
+
include CountableInstanceMethods
|
26
|
+
extend CountableClassMethods
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# This module defines instance methods for a countable model
|
31
|
+
module CountableInstanceMethods
|
32
|
+
|
33
|
+
def up!(*args)
|
34
|
+
opts = args.extract_options!
|
35
|
+
opts.merge!(vector: :up)
|
36
|
+
rotate_counter(*args, opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def down!(*args)
|
40
|
+
opts = args.extract_options!
|
41
|
+
opts.merge!(vector: :down)
|
42
|
+
rotate_counter(*args, opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def data_for(name, interval: nil, range: nil, raw: false)
|
46
|
+
self.class.data_for(name, id, interval: interval, range: range, raw: raw)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def rotate_counter(*args)
|
52
|
+
opts = args.extract_options!
|
53
|
+
name, val = args
|
54
|
+
unless respond_to?(name)
|
55
|
+
self.class.counter_error!(:not_found, name: name)
|
56
|
+
end
|
57
|
+
send(name).change(val, opts)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# This module defines class methods for a countable model
|
62
|
+
module CountableClassMethods
|
63
|
+
|
64
|
+
INTERVALS = {
|
65
|
+
year: 1.year,
|
66
|
+
month: 1.month,
|
67
|
+
week: 1.week,
|
68
|
+
day: 1.day
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
def data_for(counter_name, id = nil, interval: nil, range: nil, raw: false)
|
72
|
+
interval = normalize_interval!(counter_name, interval)
|
73
|
+
|
74
|
+
sql = <<SQL.squish!
|
75
|
+
#{cte_definition(counter_name, interval, id)}
|
76
|
+
SELECT
|
77
|
+
created_at,
|
78
|
+
COALESCE(sum(value) OVER (PARTITION BY counters.created_at) , 0) AS value
|
79
|
+
FROM (#{series_definition(interval, range)}) intervals
|
80
|
+
LEFT JOIN CTE counters USING (created_at)
|
81
|
+
ORDER BY 1
|
82
|
+
SQL
|
83
|
+
|
84
|
+
result = connection.execute(sql).to_a
|
85
|
+
|
86
|
+
raw ? result : normalize_counters_data(result)
|
87
|
+
end
|
88
|
+
|
89
|
+
def cte_definition(counter_name, interval, id = nil)
|
90
|
+
<<SQL
|
91
|
+
WITH CTE AS (
|
92
|
+
SELECT #{cte_extract(interval)} as created_at, sum(value) AS value
|
93
|
+
FROM event_counters
|
94
|
+
WHERE
|
95
|
+
countable_type = #{sanitize(name)} AND
|
96
|
+
#{ "countable_id = #{sanitize(id)} AND" if id.present? }
|
97
|
+
name = #{sanitize(counter_name)}
|
98
|
+
GROUP BY 1
|
99
|
+
)
|
100
|
+
SQL
|
101
|
+
end
|
102
|
+
|
103
|
+
def cte_extract(interval)
|
104
|
+
case interval
|
105
|
+
when Symbol
|
106
|
+
"date_trunc(#{sanitize(interval)}, created_at)"
|
107
|
+
else
|
108
|
+
tstamp(<<SQL)
|
109
|
+
floor(EXTRACT(EPOCH FROM created_at::timestamp with time zone) /
|
110
|
+
#{sanitize(interval)})::int * #{sanitize(interval)}
|
111
|
+
SQL
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def series_definition(interval, range)
|
116
|
+
range_min, range_max = min_and_max_of_range(interval, range)
|
117
|
+
|
118
|
+
args =
|
119
|
+
case interval
|
120
|
+
when Symbol
|
121
|
+
interval_sql = "interval '1 #{interval}'"
|
122
|
+
if range
|
123
|
+
[
|
124
|
+
"date_trunc(#{sanitize(interval)}, #{tstamp(range.min.to_i)} )",
|
125
|
+
"date_trunc(#{sanitize(interval)}, #{tstamp(range.max.to_i)} )",
|
126
|
+
interval_sql
|
127
|
+
]
|
128
|
+
else
|
129
|
+
[
|
130
|
+
"date_trunc(#{sanitize(interval)}, min(created_at))",
|
131
|
+
"date_trunc(#{sanitize(interval)}, max(created_at))",
|
132
|
+
interval_sql
|
133
|
+
]
|
134
|
+
end
|
135
|
+
else
|
136
|
+
interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
|
137
|
+
if range
|
138
|
+
[
|
139
|
+
tstamp(sanitize(range_min)),
|
140
|
+
tstamp(sanitize(range_max)),
|
141
|
+
interval_sql
|
142
|
+
]
|
143
|
+
else
|
144
|
+
[ 'min(created_at)', 'max(created_at)', interval_sql ]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
<<SQL
|
148
|
+
SELECT
|
149
|
+
count(*), generate_series(#{args[0]}, #{args[1] }, #{args[2]}) AS created_at
|
150
|
+
FROM CTE
|
151
|
+
SQL
|
152
|
+
end
|
153
|
+
|
154
|
+
def tstamp(val)
|
155
|
+
"to_timestamp(#{val})"
|
156
|
+
end
|
157
|
+
|
158
|
+
def counter_error!(*args)
|
159
|
+
fail EventCounter::CounterError, args
|
160
|
+
end
|
161
|
+
|
162
|
+
def normalize_interval!(counter_name, interval)
|
163
|
+
default_interval = default_interval_for(counter_name)
|
164
|
+
|
165
|
+
h = {
|
166
|
+
default_interval: default_interval,
|
167
|
+
interval: interval,
|
168
|
+
model: self.class.name
|
169
|
+
}
|
170
|
+
|
171
|
+
return default_interval.to_i unless interval
|
172
|
+
|
173
|
+
counter_error!(:not_found, name: name) unless default_interval
|
174
|
+
counter_error!(:less, h) if less_then_default?(default_interval, interval)
|
175
|
+
unless multiple_of_default?(default_interval, interval)
|
176
|
+
counter_error!(:multiple, h)
|
177
|
+
end
|
178
|
+
|
179
|
+
interval.respond_to?(:to_i) ? interval.to_i : interval
|
180
|
+
end
|
181
|
+
|
182
|
+
def less_then_default?(*args)
|
183
|
+
default, provided = args.map do |arg|
|
184
|
+
interval_as_integer(arg)
|
185
|
+
end
|
186
|
+
provided < default
|
187
|
+
end
|
188
|
+
|
189
|
+
def multiple_of_default?(default_interval, provided)
|
190
|
+
return true if provided.is_a?(Symbol)
|
191
|
+
provided.modulo(default_interval).zero?
|
192
|
+
end
|
193
|
+
|
194
|
+
def interval_as_integer(interval)
|
195
|
+
interval.is_a?(Symbol) ? INTERVALS[interval] : interval
|
196
|
+
end
|
197
|
+
|
198
|
+
def normalize_counters_data(raw_data)
|
199
|
+
raw_data.map do |i|
|
200
|
+
[ Time.parse(i['created_at']), i['value'].to_i ]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def default_interval_for(counter_name)
|
205
|
+
event_counters[counter_name.to_sym]
|
206
|
+
end
|
207
|
+
|
208
|
+
def min_and_max_of_range(interval, range)
|
209
|
+
return unless range
|
210
|
+
|
211
|
+
case interval
|
212
|
+
when Symbol
|
213
|
+
[
|
214
|
+
range.min.send(:"beginning_of_#{interval}").to_i,
|
215
|
+
range.max.send(:"end_of_#{interval}").to_i
|
216
|
+
]
|
217
|
+
else
|
218
|
+
[ range.min.floor(interval).to_i, range.max.floor(interval).to_i ]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'event_counter/version'
|
2
|
+
|
3
|
+
# This class defines model that stores all counters.
|
4
|
+
class EventCounter < ActiveRecord::Base
|
5
|
+
include EventCounterVersion
|
6
|
+
|
7
|
+
belongs_to :countable, polymorphic: true
|
8
|
+
|
9
|
+
def increase_by(val)
|
10
|
+
self.class.where(id: id).update_all(['value = value + ?', val])
|
11
|
+
increment(:value, val)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def decrease_by(decrement)
|
16
|
+
self.class.where(id: id).update_all(['value = value - ?', decrement])
|
17
|
+
decrement(:value, decrement)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset_value(val = 0)
|
22
|
+
self.class.where(id: id).update_all(['value = ?', val])
|
23
|
+
self.value = val
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.make(val = 1, on_time: nil, force: false)
|
28
|
+
on_time = normalize_on_time(on_time)
|
29
|
+
|
30
|
+
attrs = { created_at: on_time }
|
31
|
+
|
32
|
+
if force && (found = scoped_relatiion.where(attrs).first)
|
33
|
+
found.reset_value(val)
|
34
|
+
else
|
35
|
+
attrs.merge!(value: val)
|
36
|
+
scoped_relatiion.create!(attrs)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current_interval
|
41
|
+
scoped_relatiion.proxy_association.owner.event_counters[counter_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.counter_name
|
45
|
+
scoped_relatiion.proxy_association.reflection.name
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.change(val = 1, vector: :up, on_time: nil, force: nil)
|
49
|
+
counter_error!(:direction) unless [:up, :down].include?(vector)
|
50
|
+
|
51
|
+
val ||= 1
|
52
|
+
|
53
|
+
on_time = normalize_on_time(on_time)
|
54
|
+
|
55
|
+
counter = where(created_at: on_time).first
|
56
|
+
|
57
|
+
return counter.update!(vector, val, force) if counter
|
58
|
+
|
59
|
+
val = -val if vector == :down
|
60
|
+
make(val, on_time: on_time, force: force)
|
61
|
+
end
|
62
|
+
|
63
|
+
def update!(vector, val = 1, force = false)
|
64
|
+
if force
|
65
|
+
val = -val if vector == :down
|
66
|
+
reset_value(val)
|
67
|
+
else
|
68
|
+
vector == :up ? increase_by(val) : decrease_by(val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.scoped_relatiion
|
73
|
+
ActiveRecord::VERSION::MAJOR > 3 ? where(nil) : scoped
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.up!(*args)
|
77
|
+
change(:up, *args)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.down!(*args)
|
81
|
+
change(:down, *args)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.counter_error!(*args)
|
85
|
+
fail CounterError, args
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.normalize_on_time(on_time)
|
89
|
+
on_time ||= Time.now
|
90
|
+
on_time =
|
91
|
+
case current_interval
|
92
|
+
when Symbol
|
93
|
+
on_time.send(:"beginning_of_#{current_interval}")
|
94
|
+
else
|
95
|
+
on_time.floor(current_interval)
|
96
|
+
end
|
97
|
+
on_time
|
98
|
+
end
|
99
|
+
|
100
|
+
# Default error class
|
101
|
+
class CounterError < StandardError
|
102
|
+
MESSAGES = {
|
103
|
+
not_found: 'Unable to find counter (%{name}).',
|
104
|
+
direction: 'Wrong direction for counter.' \
|
105
|
+
'Possible values are :up and :down as symbols.',
|
106
|
+
less: 'Specified interval (%{interval}) could not be less then ' \
|
107
|
+
'a defined (%{default_interval}) in a countable model (%{model}).',
|
108
|
+
multiple: 'Specified interval (%{interval}) should be a multiple of ' \
|
109
|
+
'a defined (%{default_interval}) in a countable model (%{model}).'
|
110
|
+
}
|
111
|
+
|
112
|
+
attr_accessor :extra
|
113
|
+
|
114
|
+
def initialize(*args)
|
115
|
+
@msg, self.extra = args.flatten!
|
116
|
+
super(@msg)
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
@msg.is_a?(Symbol) ? MESSAGES[@msg] % extra : super
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
require 'event_counter/active_record_extension'
|
126
|
+
|
127
|
+
ActiveRecord::Base.send(:include, EventCounter::ActiveRecordExtension)
|
128
|
+
|
129
|
+
# :nodoc:
|
130
|
+
class Time
|
131
|
+
def floor(seconds = 60)
|
132
|
+
Time.at((to_f / seconds).floor * seconds)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class String
|
137
|
+
|
138
|
+
unless method_defined?(:squish!)
|
139
|
+
# Stolen from Rails
|
140
|
+
def squish!
|
141
|
+
gsub!(/\A[[:space:]]+/, '')
|
142
|
+
gsub!(/[[:space:]]+\z/, '')
|
143
|
+
gsub!(/[[:space:]]+/, ' ')
|
144
|
+
self
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EventCounter do
|
4
|
+
let(:ball) { Fabricate(:ball) }
|
5
|
+
|
6
|
+
it 'has version' do
|
7
|
+
expect(EventCounter::VERSION).to match(/\d+\.\d+\.\d+/)
|
8
|
+
end
|
9
|
+
|
10
|
+
it '#make' do
|
11
|
+
expect {
|
12
|
+
counter = ball.rotations.make(3)
|
13
|
+
expect(counter).to be_a(EventCounter)
|
14
|
+
|
15
|
+
expected = {
|
16
|
+
countable_id: ball.id,
|
17
|
+
countable_type: ball.class.name,
|
18
|
+
created_at: Time.now.floor(300),
|
19
|
+
name: 'rotations',
|
20
|
+
value: 3
|
21
|
+
}.with_indifferent_access
|
22
|
+
|
23
|
+
counter.attributes.except('id').keys.each do |attr|
|
24
|
+
expect(counter[attr]).to be_eql(expected[attr])
|
25
|
+
end
|
26
|
+
}.to change { EventCounter.count }.by(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
it '#make on time' do
|
30
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
31
|
+
expect {
|
32
|
+
counter = ball.rotations.make(
|
33
|
+
3, on_time: on_time)
|
34
|
+
expect(counter).to be_a(EventCounter)
|
35
|
+
|
36
|
+
expected = {
|
37
|
+
countable_id: ball.id,
|
38
|
+
countable_type: ball.class.name,
|
39
|
+
created_at: on_time.change(min: 10),
|
40
|
+
name: 'rotations',
|
41
|
+
value: 3
|
42
|
+
}.with_indifferent_access
|
43
|
+
|
44
|
+
counter.attributes.except('id').keys.each do |attr|
|
45
|
+
expect(counter[attr]).to be_eql(expected[attr])
|
46
|
+
end
|
47
|
+
}.to change { EventCounter.count }.by(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
it '#make on time with interval as symbol' do
|
51
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
52
|
+
[:week, :month, :year].each do |interval|
|
53
|
+
expect {
|
54
|
+
counter = ball.send(:"rotations_by_#{interval}").make(
|
55
|
+
3, on_time: on_time)
|
56
|
+
expect(counter).to be_a(EventCounter)
|
57
|
+
|
58
|
+
expected = {
|
59
|
+
countable_id: ball.id,
|
60
|
+
countable_type: ball.class.name,
|
61
|
+
created_at: on_time.send(:"beginning_of_#{interval}"),
|
62
|
+
name: "rotations_by_#{interval}",
|
63
|
+
value: 3
|
64
|
+
}.with_indifferent_access
|
65
|
+
|
66
|
+
counter.attributes.except('id').keys.each do |attr|
|
67
|
+
expect(counter[attr]).to be_eql(expected[attr])
|
68
|
+
end
|
69
|
+
}.to change { EventCounter.count }.by(1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
describe Ball do
|
76
|
+
let(:ball) { Fabricate(:ball) }
|
77
|
+
|
78
|
+
it 'creates a new counter while incrementing' do
|
79
|
+
expect {
|
80
|
+
expect(ball.up!(:rotations)).to be_a(EventCounter)
|
81
|
+
}.to change { EventCounter.count }.by(1)
|
82
|
+
|
83
|
+
on_time = Time.mktime(2011, 11, 11, 11, 11)
|
84
|
+
expect {
|
85
|
+
expect(ball.up!(:rotations, on_time: on_time))
|
86
|
+
.to be_a(EventCounter)
|
87
|
+
}.to change { EventCounter.count }.by(1)
|
88
|
+
|
89
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
90
|
+
expect {
|
91
|
+
expect(ball.up!(:rotations, 5, on_time: on_time))
|
92
|
+
.to be_a(EventCounter)
|
93
|
+
}.to change { EventCounter.count }.by(1)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'creates a new counter while decrementing' do
|
97
|
+
expect {
|
98
|
+
expect(ball.down!(:rotations)).to be_a(EventCounter)
|
99
|
+
}.to change { EventCounter.count }.by(1)
|
100
|
+
|
101
|
+
on_time = Time.mktime(2011, 11, 11, 11, 11)
|
102
|
+
expect {
|
103
|
+
expect(ball.down!(:rotations, on_time: on_time))
|
104
|
+
.to be_a(EventCounter)
|
105
|
+
}.to change { EventCounter.count }.by(1)
|
106
|
+
|
107
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
108
|
+
expect {
|
109
|
+
expect(ball.down!(:rotations, 5, on_time: on_time))
|
110
|
+
.to be_a(EventCounter)
|
111
|
+
}.to change { EventCounter.count }.by(1)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'increments existent counter with default value' do
|
115
|
+
counter = ball.rotations.make
|
116
|
+
|
117
|
+
expect {
|
118
|
+
expect {
|
119
|
+
expect(ball.up!(:rotations)).to be_a(EventCounter)
|
120
|
+
}.to change { counter.reload.value }.from(1).to(2)
|
121
|
+
}.to_not change { EventCounter.count }
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'decrements existent counter with default value' do
|
125
|
+
counter = ball.rotations.make(- 1)
|
126
|
+
|
127
|
+
expect {
|
128
|
+
expect {
|
129
|
+
expect(ball.down!(:rotations)).to be_a(EventCounter)
|
130
|
+
}.to change { counter.reload.value }.from(-1).to(-2)
|
131
|
+
}.to_not change { EventCounter.count }
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'increments existent counter by a specified value' do
|
135
|
+
counter = ball.rotations.make
|
136
|
+
|
137
|
+
expect {
|
138
|
+
expect {
|
139
|
+
expect(ball.up!(:rotations, 3)).to be_a(EventCounter)
|
140
|
+
}.to change { counter.reload.value }.from(1).to(4)
|
141
|
+
}.to_not change { EventCounter.count }
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'decrements existent counter by a specified value' do
|
145
|
+
counter = ball.rotations.make 3
|
146
|
+
|
147
|
+
expect {
|
148
|
+
expect {
|
149
|
+
expect(ball.down!(:rotations, 5)).to be_a(EventCounter)
|
150
|
+
}.to change { counter.reload.value }.from(3).to(-2)
|
151
|
+
}.to_not change { EventCounter.count }
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'increments existent counter on time with default value' do
|
155
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
156
|
+
counter = ball.rotations.make on_time: on_time
|
157
|
+
|
158
|
+
expect {
|
159
|
+
expect {
|
160
|
+
expect(ball.up!(:rotations, on_time: on_time.change(min: 14)))
|
161
|
+
}.to change { counter.reload.value }.from(1).to(2)
|
162
|
+
}.to_not change { EventCounter.count }
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'decrements existent counter on time with default value' do
|
166
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
167
|
+
counter = ball.rotations.make on_time: on_time
|
168
|
+
|
169
|
+
expect {
|
170
|
+
expect {
|
171
|
+
expect(ball.down!(:rotations, on_time: on_time.change(min: 14)))
|
172
|
+
}.to change { counter.reload.value }.from(1).to(0)
|
173
|
+
}.to_not change { EventCounter.count }
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'increments existent counter on time with specified value' do
|
177
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
178
|
+
counter = ball.rotations.make 2, on_time: on_time
|
179
|
+
|
180
|
+
expect {
|
181
|
+
expect {
|
182
|
+
expect(ball.up!(:rotations, 3, on_time: on_time.change(min: 14)))
|
183
|
+
}.to change { counter.reload.value }.from(2).to(5)
|
184
|
+
}.to_not change { EventCounter.count }
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'decrements existent counter on time with specified value' do
|
188
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
189
|
+
counter = ball.rotations.make 2, on_time: on_time
|
190
|
+
|
191
|
+
expect {
|
192
|
+
expect {
|
193
|
+
expect(ball.down!(:rotations, 3, on_time: on_time.change(min: 14)))
|
194
|
+
}.to change { counter.reload.value }.from(2).to(-1)
|
195
|
+
}.to_not change { EventCounter.count }
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'forces existent counter with new value' do
|
199
|
+
counter = ball.rotations.make
|
200
|
+
|
201
|
+
expect {
|
202
|
+
expect {
|
203
|
+
expect(ball.rotations.make(5, force: true))
|
204
|
+
.to be_a(EventCounter)
|
205
|
+
}.to change { counter.reload.value }.from(1).to(5)
|
206
|
+
}.to_not change { EventCounter.count }
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'forces existent counter on time with new value' do
|
210
|
+
on_time = Time.mktime(2012, 12, 12, 12, 12)
|
211
|
+
counter = ball.rotations.make 2, on_time: on_time
|
212
|
+
|
213
|
+
expect {
|
214
|
+
expect {
|
215
|
+
expect(ball.rotations.make(5, force: true, on_time: on_time))
|
216
|
+
.to be_a(EventCounter)
|
217
|
+
}.to change { counter.reload.value }.from(2).to(5)
|
218
|
+
}.to_not change { EventCounter.count }
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'raises error on wrong direction foc counter' do
|
222
|
+
expect { ball.send(:rotate_counter, *[:rotations, vector: :wrong_direction]) }
|
223
|
+
.to raise_error(EventCounter::CounterError, /wrong direction/i)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'raises error on unable to find counter' do
|
227
|
+
expect { ball.up!(:unknown) }
|
228
|
+
.to raise_error(EventCounter::CounterError, /unable to find/i)
|
229
|
+
end
|
230
|
+
|
231
|
+
def setup_counters(countable_count = 1)
|
232
|
+
[1, 1, 2, 3, 5, 8, 13, 21, 34].each do |n|
|
233
|
+
on_time = Time.mktime(2012, 12, 12, 12, n)
|
234
|
+
if countable_count == 1
|
235
|
+
ball.rotations.make n, on_time: on_time
|
236
|
+
else
|
237
|
+
countable_count.times do
|
238
|
+
Fabricate(:ball).rotations.make n, on_time: on_time
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
context '#data_for' do
|
245
|
+
|
246
|
+
before { setup_counters }
|
247
|
+
|
248
|
+
it 'with default interval' do
|
249
|
+
data = [
|
250
|
+
# [ minute, value ]
|
251
|
+
[ 0, 7 ],
|
252
|
+
[ 5, 13 ],
|
253
|
+
[ 10, 13 ],
|
254
|
+
[ 15, 0 ],
|
255
|
+
[ 20, 21 ],
|
256
|
+
[ 25, 0 ],
|
257
|
+
[ 30, 34 ]
|
258
|
+
]
|
259
|
+
expect(ball.data_for(:rotations)).to eql_data(data)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'with a less interval' do
|
263
|
+
expect { ball.data_for(:rotations, interval: 3.minutes) }
|
264
|
+
.to raise_error(EventCounter::CounterError, /could not be less/i)
|
265
|
+
|
266
|
+
[:week, :month, :year].each do |interval|
|
267
|
+
expect { ball.data_for(:rotations_by_two_year, interval: interval) }
|
268
|
+
.to raise_error(EventCounter::CounterError, /could not be less/i)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'with a interval which is not a multiple of original interval' do
|
273
|
+
expect { ball.data_for(:rotations, interval: 7.minutes) }
|
274
|
+
.to raise_error(EventCounter::CounterError, /multiple of/i)
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'with a greater interval' do
|
278
|
+
data = [ [ 0, 33 ], [ 20, 55 ] ]
|
279
|
+
|
280
|
+
expect(ball.data_for(:rotations, interval: 20.minutes))
|
281
|
+
.to eql_data(data)
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'with a greater interval and a time range' do
|
285
|
+
range_start = Time.mktime 2012, 12, 12, 12, 15
|
286
|
+
range_end = Time.mktime 2012, 12, 12, 12, 45
|
287
|
+
range = range_start..range_end
|
288
|
+
|
289
|
+
data = [ [ 10, 13 ], [ 20, 21 ], [ 30, 34 ], [ 40, 0] ]
|
290
|
+
|
291
|
+
expect(ball.data_for(:rotations, interval: 10.minutes, range: range))
|
292
|
+
.to eql_data(data)
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'with a greater interval as symbol' do
|
296
|
+
beginning_of_week = Time.mktime(2012, 12, 12).beginning_of_week
|
297
|
+
|
298
|
+
data = [ [ beginning_of_week, 88 ] ]
|
299
|
+
|
300
|
+
expect(ball.data_for(:rotations, interval: :week))
|
301
|
+
.to eql(data)
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
context '.data_for' do
|
307
|
+
|
308
|
+
before { setup_counters(3) }
|
309
|
+
|
310
|
+
it 'with a default interval' do
|
311
|
+
data = [
|
312
|
+
# [ minute, value ]
|
313
|
+
[ 0, 21 ],
|
314
|
+
[ 5, 39 ],
|
315
|
+
[ 10, 39 ],
|
316
|
+
[ 15, 0 ],
|
317
|
+
[ 20, 63 ],
|
318
|
+
[ 25, 0 ],
|
319
|
+
[ 30, 102 ]
|
320
|
+
]
|
321
|
+
expect(subject.data_for(:rotations)).to eql_data(data)
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'with a greater interval as symbol and a simple data' do
|
325
|
+
beginning_of_month = Time.mktime(2012, 12, 12).beginning_of_month
|
326
|
+
data = [ [ beginning_of_month, 264 ] ]
|
327
|
+
expect(subject.data_for(:rotations, interval: :month)).to eql(data)
|
328
|
+
end
|
329
|
+
|
330
|
+
it 'with a greater interval as symbol and a large data' do
|
331
|
+
EventCounter.all.each do |counter|
|
332
|
+
11.times do |x|
|
333
|
+
created_at = counter.created_at - (x + 1).months
|
334
|
+
EventCounter.create!(counter.attributes.except('id')) do |c|
|
335
|
+
c.created_at = created_at
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
data = (1..12).map { |x| [ Time.mktime(2012, x), 264 ] }
|
341
|
+
|
342
|
+
expect(subject.data_for(:rotations, interval: :month)).to eql(data)
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ball, performance: true do
|
4
|
+
|
5
|
+
let(:range_start) { (Time.mktime 2011, 12).beginning_of_month }
|
6
|
+
let(:range_end) { (Time.mktime 2012, 11).end_of_month }
|
7
|
+
let(:range) { range_start..range_end }
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
@logger = ActiveRecord::Base.logger
|
11
|
+
ActiveRecord::Base.logger = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:all) do
|
15
|
+
ActiveRecord::Base.logger = @logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup_data(countable = 10, step = 1.day)
|
19
|
+
(1..countable).map do |x|
|
20
|
+
ball = Fabricate(:ball)
|
21
|
+
|
22
|
+
(range_start.to_i..range_end.to_i).step(step) do |i|
|
23
|
+
on_time = Time.at(i)
|
24
|
+
ball.rotations.make on_time: on_time
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context '#data_for' do
|
30
|
+
|
31
|
+
before { setup_data }
|
32
|
+
|
33
|
+
it 'performance is adequate' do
|
34
|
+
data = [
|
35
|
+
{"created_at"=>"2011-12-01 00:00:00+04", "value"=>"310"},
|
36
|
+
{"created_at"=>"2012-01-01 00:00:00+04", "value"=>"310"},
|
37
|
+
{"created_at"=>"2012-02-01 00:00:00+04", "value"=>"290"},
|
38
|
+
{"created_at"=>"2012-03-01 00:00:00+04", "value"=>"310"},
|
39
|
+
{"created_at"=>"2012-04-01 00:00:00+04", "value"=>"300"},
|
40
|
+
{"created_at"=>"2012-05-01 00:00:00+04", "value"=>"310"},
|
41
|
+
{"created_at"=>"2012-06-01 00:00:00+04", "value"=>"300"},
|
42
|
+
{"created_at"=>"2012-07-01 00:00:00+04", "value"=>"310"},
|
43
|
+
{"created_at"=>"2012-08-01 00:00:00+04", "value"=>"310"},
|
44
|
+
{"created_at"=>"2012-09-01 00:00:00+04", "value"=>"300"},
|
45
|
+
{"created_at"=>"2012-10-01 00:00:00+04", "value"=>"310"},
|
46
|
+
{"created_at"=>"2012-11-01 00:00:00+04", "value"=>"300"}
|
47
|
+
]
|
48
|
+
|
49
|
+
expect {
|
50
|
+
expect(Ball.data_for(:rotations, interval: :month, range: range, raw: true))
|
51
|
+
.to eql(data)
|
52
|
+
}.to take_less_than(0.1).seconds
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'database_cleaner'
|
6
|
+
require 'fabrication'
|
7
|
+
require 'logger'
|
8
|
+
require 'event_counter'
|
9
|
+
|
10
|
+
logfile_path = File.expand_path('../../log/test.log', __FILE__)
|
11
|
+
ActiveRecord::Base.logger = Logger.new(logfile_path)
|
12
|
+
|
13
|
+
conf_local = File.expand_path('../../config/database.yml', __FILE__)
|
14
|
+
conf_ci = File.expand_path('../../config/database.ci.yml', __FILE__)
|
15
|
+
|
16
|
+
# Assume Travis CI database config if no custom one exists
|
17
|
+
conf = File.exist?(conf_local) ? conf_local : conf_ci
|
18
|
+
|
19
|
+
YAML.load(File.open(conf).read).values.each do |config|
|
20
|
+
ActiveRecord::Base.establish_connection config
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::Schema.define do
|
24
|
+
self.verbose = false
|
25
|
+
|
26
|
+
create_table :cubes, force: true
|
27
|
+
|
28
|
+
create_table :balls, force: true
|
29
|
+
|
30
|
+
create_table :event_counters, force: true do |t|
|
31
|
+
t.string :name, null: false
|
32
|
+
t.integer :value, default: 0, null: false
|
33
|
+
t.references :countable, polymorphic: true, null: false
|
34
|
+
|
35
|
+
t.datetime :created_at
|
36
|
+
end
|
37
|
+
|
38
|
+
add_index :event_counters, :countable_type
|
39
|
+
add_index :event_counters, [:countable_type, :name, :countable_id, :created_at],
|
40
|
+
name: 'composite'
|
41
|
+
end
|
42
|
+
|
43
|
+
# :nodoc:
|
44
|
+
class Ball < ActiveRecord::Base
|
45
|
+
event_counter_for :rotations, 5.minutes
|
46
|
+
event_counter_for :rotations_by_week, :week
|
47
|
+
event_counter_for :rotations_by_month, :month
|
48
|
+
event_counter_for :rotations_by_year, :year
|
49
|
+
event_counter_for :rotations_by_two_year, 2.years
|
50
|
+
end
|
51
|
+
|
52
|
+
Dir[File.expand_path('../support/*.rb', __FILE__)].each do |file|
|
53
|
+
require file
|
54
|
+
end
|
55
|
+
|
56
|
+
RSpec.configure do |config|
|
57
|
+
config.before(:suite) do
|
58
|
+
DatabaseCleaner.strategy = :transaction
|
59
|
+
DatabaseCleaner.clean_with(:truncation)
|
60
|
+
end
|
61
|
+
|
62
|
+
config.before(:each) do
|
63
|
+
DatabaseCleaner.start
|
64
|
+
end
|
65
|
+
|
66
|
+
config.after(:each) do
|
67
|
+
DatabaseCleaner.clean
|
68
|
+
end
|
69
|
+
|
70
|
+
config.filter_run_excluding performance: true
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
RSpec::Matchers.define :be_eql do |expected|
|
2
|
+
match do |actual|
|
3
|
+
if expected.is_a?(Time)
|
4
|
+
expect(expected.strftime('%d-%m-%Y %H:%M:%S'))
|
5
|
+
.to eq(actual.strftime('%d-%m-%Y %H:%M:%S'))
|
6
|
+
else
|
7
|
+
expect(expected).to eql(actual)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
diffable
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec::Matchers.define :eql_data do |expected|
|
15
|
+
match do |actual|
|
16
|
+
expected.map! { |a, b| [ Time.mktime(2012, 12, 12, 12, a), b ] }
|
17
|
+
expect(actual).to eql(expected)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'benchmark'
|
22
|
+
|
23
|
+
RSpec::Matchers.define :take_less_than do |expected|
|
24
|
+
chain :seconds do; end
|
25
|
+
match do |block|
|
26
|
+
elapsed = Benchmark.realtime { block.call }
|
27
|
+
elapsed <= expected
|
28
|
+
end
|
29
|
+
|
30
|
+
supports_block_expectations
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: event-counter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anton Orel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: fabrication
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '10.0'
|
111
|
+
description: Database based event counter with throttling per time intervals
|
112
|
+
email:
|
113
|
+
- eagle.anton@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rubocop.yml"
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- config/database.ci.yml
|
125
|
+
- event-counter.gemspec
|
126
|
+
- lib/event-counter.rb
|
127
|
+
- lib/event_counter.rb
|
128
|
+
- lib/event_counter/active_record_extension.rb
|
129
|
+
- lib/event_counter/version.rb
|
130
|
+
- spec/lib/event_counter_spec.rb
|
131
|
+
- spec/lib/performance_spec.rb
|
132
|
+
- spec/spec_helper.rb
|
133
|
+
- spec/support/fabrications.rb
|
134
|
+
- spec/support/matchers.rb
|
135
|
+
homepage: https://github.com/skyeagle/event-counter
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata: {}
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
require_paths:
|
142
|
+
- lib
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 2.2.1
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: Event counter with throttling per time interval
|
159
|
+
test_files:
|
160
|
+
- spec/lib/event_counter_spec.rb
|
161
|
+
- spec/lib/performance_spec.rb
|
162
|
+
- spec/spec_helper.rb
|
163
|
+
- spec/support/fabrications.rb
|
164
|
+
- spec/support/matchers.rb
|
165
|
+
has_rdoc:
|