counter-cache 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +32 -2
- data/Rakefile +5 -1
- data/gemfiles/activerecord_3.gemfile.lock +4 -1
- data/gemfiles/activerecord_4.0.gemfile.lock +4 -1
- data/gemfiles/activerecord_4.1.gemfile.lock +50 -40
- data/lib/counter/cache/counters/buffer_counter/enqueuer.rb +8 -6
- data/lib/counter/cache/counters/buffer_counter/saver.rb +1 -0
- data/lib/counter/cache/options_parser.rb +4 -0
- data/lib/counter/cache/version.rb +1 -1
- data/spec/lib/counter/cache/buffer_counter/enqueuer_spec.rb +83 -29
- data/spec/lib/counter/cache/buffer_counter/saver_spec.rb +20 -5
- data/spec/lib/counter/cache/options_parser_spec.rb +7 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f919c6ae2950453f8c265ef31e5f60cbe492c47
|
|
4
|
+
data.tar.gz: baaf7090d810fe10ced19ce76928ce5d911f250a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f800e2e2fd6dbd1c9c60a6dd9f9a91fd84f1b42960e961631e8e23ae9ed5dd849cd6b662bcecd9c46db8e0772e13c9576465c8ac0e5839552c466b3746131957
|
|
7
|
+
data.tar.gz: 38275d8b622ebe622e2106a1b8118a4cb89901409e740ee67611fc485479535127adb55e74b565f94b96eb478337619b2d1975a3d0306a75d679de6544050ab0
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
# Counter::Cache
|
|
1
|
+
# Counter::Cache
|
|
2
2
|
|
|
3
|
-
[](http://badge.fury.io/rb/ventable)
|
|
4
|
+
[](https://travis-ci.org/wanelo/counter-cache)
|
|
5
|
+
[](https://codeclimate.com/github/wanelo/counter-cache)
|
|
4
6
|
|
|
5
7
|
Counting things is hard, counting them at scale is even harder, so control when things are counted.
|
|
6
8
|
|
|
9
|
+
Any time your application performs pagination, the underlying library probably issues a `select count(*) from ...`
|
|
10
|
+
request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium
|
|
11
|
+
dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the
|
|
12
|
+
database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap
|
|
13
|
+
RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and
|
|
14
|
+
slower, start to pile up in various queues, and eventually saturate all of the app servers.
|
|
15
|
+
There you are, the site is down.
|
|
16
|
+
|
|
17
|
+
This gem provides a solution that works at scale, and will help you keep your site up.
|
|
18
|
+
|
|
19
|
+
This library is battle-tested at Wanelo, where it has been running for several years.
|
|
20
|
+
|
|
21
|
+
### Overview
|
|
22
|
+
|
|
7
23
|
[Rails Counter Caches](http://railscasts.com/episodes/23-counter-cache-column) are a convenient way to keep counters on
|
|
8
24
|
models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails
|
|
9
25
|
counter caches create update contention on singe models, especially for social sites where any single model might become
|
|
@@ -98,6 +114,19 @@ Counter caches are configured on the models from the perspective of the child mo
|
|
|
98
114
|
|
|
99
115
|
#### Basic Counter with recalculation:
|
|
100
116
|
|
|
117
|
+
```ruby
|
|
118
|
+
class Post
|
|
119
|
+
include Counter::Cache
|
|
120
|
+
|
|
121
|
+
counter_cache_on column: :posts_count, # users.posts_count
|
|
122
|
+
relation: :user,
|
|
123
|
+
relation_class_name: "User",
|
|
124
|
+
method: :calculate_posts_count # This is a method on the user.
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### To allow a timestamp to be updated:
|
|
129
|
+
|
|
101
130
|
```ruby
|
|
102
131
|
class Post
|
|
103
132
|
include Counter::Cache
|
|
@@ -106,6 +135,7 @@ class Post
|
|
|
106
135
|
relation: :user,
|
|
107
136
|
relation_class_name: "User",
|
|
108
137
|
method: :calculate_posts_count, # This is a method on the user.
|
|
138
|
+
touch_column: :posts_updated_at
|
|
109
139
|
end
|
|
110
140
|
```
|
|
111
141
|
|
data/Rakefile
CHANGED
|
@@ -1,84 +1,91 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../
|
|
3
3
|
specs:
|
|
4
|
-
counter-cache (0.0
|
|
4
|
+
counter-cache (0.3.0)
|
|
5
5
|
activerecord (>= 3.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activemodel (4.1.
|
|
11
|
-
activesupport (= 4.1.
|
|
10
|
+
activemodel (4.1.14.1)
|
|
11
|
+
activesupport (= 4.1.14.1)
|
|
12
12
|
builder (~> 3.1)
|
|
13
|
-
activerecord (4.1.
|
|
14
|
-
activemodel (= 4.1.
|
|
15
|
-
activesupport (= 4.1.
|
|
13
|
+
activerecord (4.1.14.1)
|
|
14
|
+
activemodel (= 4.1.14.1)
|
|
15
|
+
activesupport (= 4.1.14.1)
|
|
16
16
|
arel (~> 5.0.0)
|
|
17
|
-
activesupport (4.1.
|
|
17
|
+
activesupport (4.1.14.1)
|
|
18
18
|
i18n (~> 0.6, >= 0.6.9)
|
|
19
19
|
json (~> 1.7, >= 1.7.7)
|
|
20
20
|
minitest (~> 5.1)
|
|
21
21
|
thread_safe (~> 0.1)
|
|
22
22
|
tzinfo (~> 1.1)
|
|
23
|
-
appraisal (1.0
|
|
23
|
+
appraisal (2.1.0)
|
|
24
24
|
bundler
|
|
25
25
|
rake
|
|
26
26
|
thor (>= 0.14.0)
|
|
27
27
|
arel (5.0.1.20140414130214)
|
|
28
28
|
builder (3.2.2)
|
|
29
|
-
celluloid (0.15.2)
|
|
30
|
-
timers (~> 1.1.0)
|
|
31
29
|
coderay (1.1.0)
|
|
32
30
|
diff-lcs (1.2.5)
|
|
33
31
|
fakeredis (0.5.0)
|
|
34
32
|
redis (~> 3.0)
|
|
35
|
-
ffi (1.9.
|
|
33
|
+
ffi (1.9.10)
|
|
36
34
|
formatador (0.2.5)
|
|
37
|
-
guard (2.
|
|
35
|
+
guard (2.13.0)
|
|
38
36
|
formatador (>= 0.2.4)
|
|
39
|
-
listen (
|
|
37
|
+
listen (>= 2.7, <= 4.0)
|
|
40
38
|
lumberjack (~> 1.0)
|
|
39
|
+
nenv (~> 0.1)
|
|
40
|
+
notiffany (~> 0.0)
|
|
41
41
|
pry (>= 0.9.12)
|
|
42
|
+
shellany (~> 0.0)
|
|
42
43
|
thor (>= 0.18.1)
|
|
43
|
-
guard-
|
|
44
|
+
guard-compat (1.2.1)
|
|
45
|
+
guard-rspec (4.6.4)
|
|
44
46
|
guard (~> 2.1)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
guard-compat (~> 1.1)
|
|
48
|
+
rspec (>= 2.99.0, < 4.0)
|
|
49
|
+
i18n (0.7.0)
|
|
50
|
+
json (1.8.3)
|
|
51
|
+
listen (3.0.5)
|
|
50
52
|
rb-fsevent (>= 0.9.3)
|
|
51
53
|
rb-inotify (>= 0.9)
|
|
52
|
-
lumberjack (1.0.
|
|
54
|
+
lumberjack (1.0.10)
|
|
53
55
|
method_source (0.8.2)
|
|
54
|
-
minitest (5.4
|
|
55
|
-
|
|
56
|
+
minitest (5.8.4)
|
|
57
|
+
nenv (0.2.0)
|
|
58
|
+
notiffany (0.0.8)
|
|
59
|
+
nenv (~> 0.1)
|
|
60
|
+
shellany (~> 0.0)
|
|
61
|
+
pry (0.10.3)
|
|
56
62
|
coderay (~> 1.1.0)
|
|
57
63
|
method_source (~> 0.8.1)
|
|
58
64
|
slop (~> 3.4)
|
|
59
|
-
rake (10.
|
|
60
|
-
rb-fsevent (0.9.
|
|
65
|
+
rake (10.5.0)
|
|
66
|
+
rb-fsevent (0.9.7)
|
|
61
67
|
rb-inotify (0.9.5)
|
|
62
68
|
ffi (>= 0.5.0)
|
|
63
|
-
redis (3.
|
|
64
|
-
rspec (3.
|
|
65
|
-
rspec-core (~> 3.
|
|
66
|
-
rspec-expectations (~> 3.
|
|
67
|
-
rspec-mocks (~> 3.
|
|
68
|
-
rspec-core (3.
|
|
69
|
-
rspec-support (~> 3.
|
|
70
|
-
rspec-expectations (3.0
|
|
69
|
+
redis (3.2.2)
|
|
70
|
+
rspec (3.4.0)
|
|
71
|
+
rspec-core (~> 3.4.0)
|
|
72
|
+
rspec-expectations (~> 3.4.0)
|
|
73
|
+
rspec-mocks (~> 3.4.0)
|
|
74
|
+
rspec-core (3.4.2)
|
|
75
|
+
rspec-support (~> 3.4.0)
|
|
76
|
+
rspec-expectations (3.4.0)
|
|
71
77
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
72
|
-
rspec-support (~> 3.
|
|
73
|
-
rspec-mocks (3.
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
rspec-support (~> 3.4.0)
|
|
79
|
+
rspec-mocks (3.4.1)
|
|
80
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
81
|
+
rspec-support (~> 3.4.0)
|
|
82
|
+
rspec-support (3.4.1)
|
|
83
|
+
shellany (0.0.1)
|
|
76
84
|
slop (3.6.0)
|
|
77
|
-
sqlite3 (1.3.
|
|
85
|
+
sqlite3 (1.3.11)
|
|
78
86
|
thor (0.19.1)
|
|
79
|
-
thread_safe (0.3.
|
|
80
|
-
|
|
81
|
-
tzinfo (1.2.1)
|
|
87
|
+
thread_safe (0.3.5)
|
|
88
|
+
tzinfo (1.2.2)
|
|
82
89
|
thread_safe (~> 0.1)
|
|
83
90
|
|
|
84
91
|
PLATFORMS
|
|
@@ -95,3 +102,6 @@ DEPENDENCIES
|
|
|
95
102
|
rake
|
|
96
103
|
rspec (>= 3.0)
|
|
97
104
|
sqlite3
|
|
105
|
+
|
|
106
|
+
BUNDLED WITH
|
|
107
|
+
1.11.2
|
|
@@ -11,14 +11,16 @@ module Counter
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def create_and_enqueue(delay, cached)
|
|
14
|
+
parameters = { relation_class_name: relation_class,
|
|
15
|
+
relation_id: relation_id,
|
|
16
|
+
column: options.column,
|
|
17
|
+
method: options.method,
|
|
18
|
+
cache: cached,
|
|
19
|
+
counter: counter_class_name }
|
|
20
|
+
parameters[:touch_column] = options.touch_column unless options.touch_column.nil?
|
|
14
21
|
options.worker_adapter.enqueue(delay,
|
|
15
22
|
source_object_class_name,
|
|
16
|
-
|
|
17
|
-
relation_id: relation_id,
|
|
18
|
-
column: options.column,
|
|
19
|
-
method: options.method,
|
|
20
|
-
cache: cached,
|
|
21
|
-
counter: counter_class_name })
|
|
23
|
+
parameters)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
|
@@ -2,52 +2,106 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
RSpec.describe Counter::Cache::Counters::BufferCounter::Enqueuer do
|
|
4
4
|
let(:worker_adapter) { double }
|
|
5
|
-
let(:options) { double(worker_adapter: worker_adapter,
|
|
6
|
-
wait: 10,
|
|
7
|
-
column: "boo",
|
|
8
|
-
method: "calculate_boo",
|
|
9
|
-
cached?: true,
|
|
10
|
-
recalculation?: false,
|
|
11
|
-
recalculation_delay: 20) }
|
|
12
5
|
|
|
13
6
|
let(:source_object_class_name) { "BooUser" }
|
|
14
7
|
let(:relation_id) { 1 }
|
|
15
8
|
let(:relation_type) { "Boo" }
|
|
16
9
|
|
|
17
|
-
let(:enqueuer) { Counter::Cache::Counters::BufferCounter::Enqueuer.new(options, source_object_class_name, relation_id, relation_type, "SuperCounter") }
|
|
18
|
-
|
|
19
10
|
describe '#enqueue' do
|
|
20
|
-
|
|
21
|
-
expect(worker_adapter).to receive(:enqueue).with(10,
|
|
22
|
-
"BooUser",
|
|
23
|
-
{ relation_class_name: "Boo",
|
|
24
|
-
relation_id: 1,
|
|
25
|
-
column: "boo",
|
|
26
|
-
method: "calculate_boo",
|
|
27
|
-
cache: true,
|
|
28
|
-
counter: "SuperCounter" })
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
describe 'when recalculation is true' do
|
|
11
|
+
context "without touch column" do
|
|
32
12
|
before do
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
@options = double(worker_adapter: worker_adapter,
|
|
14
|
+
wait: 10,
|
|
15
|
+
column: "boo",
|
|
16
|
+
method: "calculate_boo",
|
|
17
|
+
cached?: true,
|
|
18
|
+
recalculation?: false,
|
|
19
|
+
recalculation_delay: 20)
|
|
20
|
+
|
|
21
|
+
@enqueuer = Counter::Cache::Counters::BufferCounter::Enqueuer.new(@options, source_object_class_name, relation_id, relation_type, "SuperCounter")
|
|
35
22
|
|
|
36
|
-
|
|
37
|
-
expect(worker_adapter).to receive(:enqueue).with(20,
|
|
23
|
+
expect(worker_adapter).to receive(:enqueue).with(10,
|
|
38
24
|
"BooUser",
|
|
39
25
|
{ relation_class_name: "Boo",
|
|
40
26
|
relation_id: 1,
|
|
41
27
|
column: "boo",
|
|
42
28
|
method: "calculate_boo",
|
|
43
|
-
cache:
|
|
29
|
+
cache: true,
|
|
44
30
|
counter: "SuperCounter" })
|
|
45
|
-
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe 'when recalculation is true' do
|
|
34
|
+
before do
|
|
35
|
+
expect(@options).to receive(:recalculation?).and_return(true)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "enqueues two jobs" do
|
|
39
|
+
expect(@options).to receive(:touch_column).twice
|
|
40
|
+
expect(worker_adapter).to receive(:enqueue).with(20,
|
|
41
|
+
"BooUser",
|
|
42
|
+
{ relation_class_name: "Boo",
|
|
43
|
+
relation_id: 1,
|
|
44
|
+
column: "boo",
|
|
45
|
+
method: "calculate_boo",
|
|
46
|
+
cache: false,
|
|
47
|
+
counter: "SuperCounter" })
|
|
48
|
+
@enqueuer.enqueue!(double)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'enqueues one job' do
|
|
53
|
+
expect(@options).to receive(:touch_column)
|
|
54
|
+
@enqueuer.enqueue!(double)
|
|
46
55
|
end
|
|
47
56
|
end
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
context "with touch column" do
|
|
59
|
+
before(:each) do
|
|
60
|
+
@options = double(worker_adapter: worker_adapter,
|
|
61
|
+
wait: 10,
|
|
62
|
+
column: "boo",
|
|
63
|
+
touch_column: "boo_updated_at",
|
|
64
|
+
method: "calculate_boo",
|
|
65
|
+
cached?: true,
|
|
66
|
+
recalculation?: false,
|
|
67
|
+
recalculation_delay: 20)
|
|
68
|
+
@enqueuer = Counter::Cache::Counters::BufferCounter::Enqueuer.new(@options, source_object_class_name, relation_id, relation_type, "SuperCounter")
|
|
69
|
+
|
|
70
|
+
expect(worker_adapter).to receive(:enqueue).with(10,
|
|
71
|
+
"BooUser",
|
|
72
|
+
{ relation_class_name: "Boo",
|
|
73
|
+
relation_id: 1,
|
|
74
|
+
column: "boo",
|
|
75
|
+
touch_column: "boo_updated_at",
|
|
76
|
+
method: "calculate_boo",
|
|
77
|
+
cache: true,
|
|
78
|
+
counter: "SuperCounter" })
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe 'when recalculation is true' do
|
|
82
|
+
before do
|
|
83
|
+
expect(@options).to receive(:recalculation?).and_return(true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "enqueues two jobs" do
|
|
87
|
+
expect(@options).to receive(:touch_column).exactly(4).times
|
|
88
|
+
expect(worker_adapter).to receive(:enqueue).with(20,
|
|
89
|
+
"BooUser",
|
|
90
|
+
{ relation_class_name: "Boo",
|
|
91
|
+
relation_id: 1,
|
|
92
|
+
column: "boo",
|
|
93
|
+
touch_column: "boo_updated_at",
|
|
94
|
+
method: "calculate_boo",
|
|
95
|
+
cache: false,
|
|
96
|
+
counter: "SuperCounter" })
|
|
97
|
+
@enqueuer.enqueue!(double)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'enqueues one job' do
|
|
102
|
+
expect(@options).to receive(:touch_column)
|
|
103
|
+
@enqueuer.enqueue!(double)
|
|
104
|
+
end
|
|
51
105
|
end
|
|
52
106
|
end
|
|
53
107
|
end
|
|
@@ -4,7 +4,7 @@ RSpec.describe Counter::Cache::Counters::BufferCounter::Saver do
|
|
|
4
4
|
class Boo
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
-
let(:relation_object) { double(boo_count: 2) }
|
|
7
|
+
let(:relation_object) { double(boo_count: 2, boo_updated_at: DateTime.now) }
|
|
8
8
|
let(:options) { double(relation_class_name: "Boo", relation_id: 1, column: "boo_count", method: nil, source_object_class_name: Boo) }
|
|
9
9
|
let(:saver) { Counter::Cache::Counters::BufferCounter::Saver.new(options) }
|
|
10
10
|
|
|
@@ -21,6 +21,7 @@ RSpec.describe Counter::Cache::Counters::BufferCounter::Saver do
|
|
|
21
21
|
|
|
22
22
|
before do
|
|
23
23
|
allow(options).to receive(:cached?).and_return(true)
|
|
24
|
+
allow(options).to receive(:touch_column).and_return(nil)
|
|
24
25
|
expect(relation_object).to receive(:boo_count=).with(4)
|
|
25
26
|
expect(relation_object).to receive(:save!)
|
|
26
27
|
expect(counting_store).to receive(:del)
|
|
@@ -41,15 +42,29 @@ RSpec.describe Counter::Cache::Counters::BufferCounter::Saver do
|
|
|
41
42
|
expect(counting_store).to receive(:del)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
context "with touch column" do
|
|
46
|
+
it 'saves the value' do
|
|
47
|
+
allow(options).to receive(:touch_column).and_return(:boo_updated_at)
|
|
48
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
|
49
|
+
expect(relation_object).to receive(:boo_updated_at=).with(DateTime.now)
|
|
50
|
+
expect(relation_object).to receive(:save!)
|
|
51
|
+
saver.save!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context "without touch column" do
|
|
56
|
+
it 'saves the value' do
|
|
57
|
+
allow(options).to receive(:touch_column).and_return(nil)
|
|
58
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
|
59
|
+
expect(relation_object).to receive(:save!)
|
|
60
|
+
saver.save!
|
|
61
|
+
end
|
|
48
62
|
end
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
describe 'when cached? is false' do
|
|
52
66
|
before do
|
|
67
|
+
allow(options).to receive(:touch_column).and_return(nil)
|
|
53
68
|
allow(options).to receive(:cached?).and_return(false)
|
|
54
69
|
end
|
|
55
70
|
|
|
@@ -34,6 +34,13 @@ RSpec.describe Counter::Cache::OptionsParser do
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
describe "#touch_column" do
|
|
38
|
+
let(:options) { { touch_column: "touch_column_name" } }
|
|
39
|
+
it "returns option if set" do
|
|
40
|
+
expect(parser.touch_column).to eq("touch_column_name")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
describe "#method" do
|
|
38
45
|
let(:options) { { method: "method" } }
|
|
39
46
|
it "returns option if set" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: counter-cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Henry & Matt Camuto
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2016-02-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -207,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
207
207
|
version: '0'
|
|
208
208
|
requirements: []
|
|
209
209
|
rubyforge_project:
|
|
210
|
-
rubygems_version: 2.
|
|
210
|
+
rubygems_version: 2.5.1
|
|
211
211
|
signing_key:
|
|
212
212
|
specification_version: 4
|
|
213
213
|
summary: Counting is hard.
|