event-counter 0.0.1
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.
- 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:
|