counter-cache 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 +22 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/counter-cache.gemspec +30 -0
- data/lib/counter/cache.rb +28 -0
- data/lib/counter/cache/active_record_updater.rb +23 -0
- data/lib/counter/cache/config.rb +12 -0
- data/lib/counter/cache/counters/buffer_counter.rb +25 -0
- data/lib/counter/cache/counters/buffer_counter/enqueuer.rb +27 -0
- data/lib/counter/cache/counters/buffer_counter/key.rb +23 -0
- data/lib/counter/cache/counters/buffer_counter/relation_finder.rb +29 -0
- data/lib/counter/cache/counters/buffer_counter/saver.rb +70 -0
- data/lib/counter/cache/counters/buffer_counter/updater.rb +54 -0
- data/lib/counter/cache/options_parser.rb +69 -0
- data/lib/counter/cache/redis.rb +40 -0
- data/lib/counter/cache/version.rb +5 -0
- data/spec/features/counter_spec.rb +95 -0
- data/spec/lib/counter/cache/active_record_updater_spec.rb +26 -0
- data/spec/lib/counter/cache/buffer_counter/enqueuer_spec.rb +53 -0
- data/spec/lib/counter/cache/buffer_counter/key_spec.rb +13 -0
- data/spec/lib/counter/cache/buffer_counter/relation_finder_spec.rb +49 -0
- data/spec/lib/counter/cache/buffer_counter/saver_spec.rb +84 -0
- data/spec/lib/counter/cache/buffer_counter/updater_spec.rb +96 -0
- data/spec/lib/counter/cache/buffer_counter_spec.rb +27 -0
- data/spec/lib/counter/cache/config_spec.rb +36 -0
- data/spec/lib/counter/cache/options_parser_spec.rb +176 -0
- data/spec/lib/counter/cache/redis_spec.rb +44 -0
- data/spec/lib/counter/cache_spec.rb +14 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/models.rb +87 -0
- data/spec/support/worker_adapter.rb +8 -0
- metadata +208 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module Counter
|
2
|
+
module Cache
|
3
|
+
class OptionsParser < Struct.new(:options)
|
4
|
+
def worker_adapter
|
5
|
+
options[:worker_adapter] || Counter::Cache.configuration.default_worker_adapter
|
6
|
+
end
|
7
|
+
|
8
|
+
def source_object_class_name
|
9
|
+
options[:source_object_class_name]
|
10
|
+
end
|
11
|
+
|
12
|
+
def column
|
13
|
+
options[:column]
|
14
|
+
end
|
15
|
+
|
16
|
+
def relation
|
17
|
+
options[:relation]
|
18
|
+
end
|
19
|
+
|
20
|
+
def relation_class_name
|
21
|
+
options[:relation_class_name]
|
22
|
+
end
|
23
|
+
|
24
|
+
def relation_id
|
25
|
+
options[:relation_id]
|
26
|
+
end
|
27
|
+
|
28
|
+
def method
|
29
|
+
options[:method]
|
30
|
+
end
|
31
|
+
|
32
|
+
def cached?
|
33
|
+
option_or_true options[:cache]
|
34
|
+
end
|
35
|
+
|
36
|
+
def recalculation?
|
37
|
+
option_or_true options[:recalculation]
|
38
|
+
end
|
39
|
+
|
40
|
+
def polymorphic?
|
41
|
+
options[:polymorphic]
|
42
|
+
end
|
43
|
+
|
44
|
+
def if_value
|
45
|
+
options[:if]
|
46
|
+
end
|
47
|
+
|
48
|
+
def wait(source_object)
|
49
|
+
wait = options[:wait]
|
50
|
+
if wait.respond_to?(:call)
|
51
|
+
wait.call(source_object)
|
52
|
+
else
|
53
|
+
wait
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def recalculation_delay
|
58
|
+
options[:recalculation_delay] || Counter::Cache.configuration.recalculation_delay
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def option_or_true(val)
|
64
|
+
val || val.nil?
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Counter
|
2
|
+
module Cache
|
3
|
+
class Redis
|
4
|
+
def incr(key)
|
5
|
+
with_redis do |redis|
|
6
|
+
redis.incr key
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def decr(key)
|
11
|
+
with_redis do |redis|
|
12
|
+
redis.decr(key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(key)
|
17
|
+
with_redis do |redis|
|
18
|
+
redis.get(key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def del(key)
|
23
|
+
with_redis do |redis|
|
24
|
+
redis.del(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def with_redis
|
31
|
+
redis_pool = Counter::Cache.configuration.redis_pool
|
32
|
+
return yield redis_pool unless redis_pool.respond_to?(:with)
|
33
|
+
|
34
|
+
redis_pool.with do |redis|
|
35
|
+
yield redis
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/models'
|
3
|
+
require 'support/worker_adapter'
|
4
|
+
require 'fakeredis'
|
5
|
+
|
6
|
+
RSpec.describe "Counting" do
|
7
|
+
|
8
|
+
before do
|
9
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:up) }
|
10
|
+
Counter::Cache.configure do |c|
|
11
|
+
c.redis_pool = Redis.new
|
12
|
+
c.default_worker_adapter = TestWorkerAdapter.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:down) }
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:user) { User.create }
|
21
|
+
|
22
|
+
describe '#posts_count' do
|
23
|
+
it 'increments' do
|
24
|
+
expect {
|
25
|
+
user.posts.create
|
26
|
+
}.to change { user.reload.posts_count }.by(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'decrements' do
|
30
|
+
post = user.posts.create
|
31
|
+
expect {
|
32
|
+
post.destroy
|
33
|
+
}.to change { user.reload.posts_count }.by(-1)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#posts_ar_count' do
|
38
|
+
it 'increments' do
|
39
|
+
expect {
|
40
|
+
user.posts.create
|
41
|
+
}.to change { user.reload.posts_ar_count }.by(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'decrements' do
|
45
|
+
post = user.posts.create
|
46
|
+
expect {
|
47
|
+
post.destroy
|
48
|
+
}.to change { user.reload.posts_ar_count }.by(-1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#polymorphic followers_count' do
|
53
|
+
let(:follower_user) { User.create }
|
54
|
+
it 'increments' do
|
55
|
+
expect {
|
56
|
+
Follow.create(user: follower_user, followee: user)
|
57
|
+
}.to change { user.reload.followers_count }.by(1)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'decrements' do
|
61
|
+
follow = Follow.create(user: follower_user, followee: user)
|
62
|
+
expect {
|
63
|
+
follow.destroy
|
64
|
+
}.to change { user.reload.followers_count }.by(-1)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#users_i_follow_count' do
|
69
|
+
let(:follower_user) { User.create }
|
70
|
+
|
71
|
+
it 'increments' do
|
72
|
+
expect {
|
73
|
+
Follow.create(user: follower_user, followee: user)
|
74
|
+
}.to change { follower_user.reload.users_i_follow_count }.by(1)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'decrements' do
|
78
|
+
follow = Follow.create(user: follower_user, followee: user)
|
79
|
+
expect {
|
80
|
+
follow.destroy
|
81
|
+
}.to change { follower_user.reload.users_i_follow_count }.by(-1)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#bogus_followed_count' do
|
86
|
+
let(:follower_user) { User.create }
|
87
|
+
|
88
|
+
it 'eventually recalculates' do
|
89
|
+
expect(user.reload.bogus_followed_count).to_not eq(101)
|
90
|
+
Follow.create(user: follower_user, followee: user)
|
91
|
+
expect(user.reload.bogus_followed_count).to eq(101)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::ActiveRecordUpdater do
|
4
|
+
let(:counter_class) { double }
|
5
|
+
let(:counter) { double }
|
6
|
+
let(:options) { { counter_class: counter_class } }
|
7
|
+
subject { Counter::Cache::ActiveRecordUpdater.new(options) }
|
8
|
+
|
9
|
+
let(:record) { double }
|
10
|
+
|
11
|
+
describe "#after_create" do
|
12
|
+
it "Calls update on counter instance" do
|
13
|
+
expect(counter).to receive(:update).with(:incr)
|
14
|
+
expect(counter_class).to receive(:new).with(record, options).and_return(counter)
|
15
|
+
subject.after_create(record)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#after_destroy" do
|
20
|
+
it "Calls update on counter instance" do
|
21
|
+
expect(counter).to receive(:update).with(:decr)
|
22
|
+
expect(counter_class).to receive(:new).with(record, options).and_return(counter)
|
23
|
+
subject.after_destroy(record)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::Counters::BufferCounter::Enqueuer do
|
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
|
+
|
13
|
+
let(:source_object_class_name) { "BooUser" }
|
14
|
+
let(:relation_id) { 1 }
|
15
|
+
let(:relation_type) { "Boo" }
|
16
|
+
|
17
|
+
let(:enqueuer) { Counter::Cache::Counters::BufferCounter::Enqueuer.new(options, source_object_class_name, relation_id, relation_type, "SuperCounter") }
|
18
|
+
|
19
|
+
describe '#enqueue' do
|
20
|
+
before do
|
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
|
32
|
+
before do
|
33
|
+
expect(options).to receive(:recalculation?).and_return(true)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "enqueues two jobs" do
|
37
|
+
expect(worker_adapter).to receive(:enqueue).with(20,
|
38
|
+
"BooUser",
|
39
|
+
{ relation_class_name: "Boo",
|
40
|
+
relation_id: 1,
|
41
|
+
column: "boo",
|
42
|
+
method: "calculate_boo",
|
43
|
+
cache: false,
|
44
|
+
counter: "SuperCounter" })
|
45
|
+
enqueuer.enqueue!(double)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'enqueues one job' do
|
50
|
+
enqueuer.enqueue!(double)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::Counters::BufferCounter::Key do
|
4
|
+
let(:options) { double(relation_class_name: "Boo", relation: "boo", column: "boos_count", relation_id: nil) }
|
5
|
+
let(:source_object) { double(boo_id: 1) }
|
6
|
+
let(:key) { Counter::Cache::Counters::BufferCounter::Key.new(source_object, options) }
|
7
|
+
|
8
|
+
describe '#to_s' do
|
9
|
+
it 'returns the key with the class, id, and column' do
|
10
|
+
expect(key.to_s).to eq("cc:Bo:1:boos")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::Counters::BufferCounter::RelationFinder do
|
4
|
+
let(:options) { double }
|
5
|
+
let(:source_object) { double }
|
6
|
+
let(:finder) { Counter::Cache::Counters::BufferCounter::RelationFinder.new(source_object, options) }
|
7
|
+
|
8
|
+
describe '#relation_class' do
|
9
|
+
context 'when relation_class_name is present' do
|
10
|
+
let(:options) { double(relation_class_name: "Boo") }
|
11
|
+
|
12
|
+
it 'returns the relation_class_name' do
|
13
|
+
expect(finder.relation_class).to eq("Boo")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when polymorphic?' do
|
18
|
+
let(:options) { double(polymorphic?: true, relation: "boo", relation_class_name: nil) }
|
19
|
+
let(:source_object) { double(boo_type: "Boo") }
|
20
|
+
|
21
|
+
it 'asks for the type' do
|
22
|
+
expect(finder.relation_class).to eq("Boo")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'no relation_class_name or polymorphic' do
|
27
|
+
let(:options) { double(relation_class_name: nil, polymorphic?: false, relation: "boo") }
|
28
|
+
|
29
|
+
before do
|
30
|
+
reflection = double
|
31
|
+
expect(reflection).to receive_message_chain("class_name.to_s.camelize") { "Boo" }
|
32
|
+
expect(source_object).to receive(:reflections).and_return({:boo => reflection})
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'asks active record for the class name' do
|
36
|
+
expect(finder.relation_class).to eq("Boo")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#relation_id' do
|
42
|
+
let(:options) { double(relation: "boo", relation_id: nil) }
|
43
|
+
let(:source_object) { double(boo_id: 123) }
|
44
|
+
|
45
|
+
it 'calls relation_id on the source object' do
|
46
|
+
expect(finder.relation_id).to eq(123)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::Counters::BufferCounter::Saver do
|
4
|
+
class Boo
|
5
|
+
end
|
6
|
+
|
7
|
+
let(:relation_object) { double(boo_count: 2) }
|
8
|
+
let(:options) { double(relation_class_name: "Boo", relation_id: 1, column: "boo_count", method: nil, source_object_class_name: Boo) }
|
9
|
+
let(:saver) { Counter::Cache::Counters::BufferCounter::Saver.new(options) }
|
10
|
+
|
11
|
+
describe '#save!' do
|
12
|
+
let(:counting_store) { double(get: nil) }
|
13
|
+
|
14
|
+
before do
|
15
|
+
allow(Boo).to receive(:find_by_id).and_return(relation_object)
|
16
|
+
allow(Counter::Cache.configuration).to receive(:counting_data_store).and_return(counting_store)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "yielding" do
|
20
|
+
let(:counting_store) { double(get: 2) }
|
21
|
+
|
22
|
+
before do
|
23
|
+
allow(options).to receive(:cached?).and_return(true)
|
24
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
25
|
+
expect(relation_object).to receive(:save!)
|
26
|
+
expect(counting_store).to receive(:del)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "with block" do
|
30
|
+
it "yields" do
|
31
|
+
expect { |b| saver.save!(&b) }.to yield_with_args(2, 4, relation_object, "boo_count")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'when cached? is true' do
|
37
|
+
let(:counting_store) { double(get: 2) }
|
38
|
+
|
39
|
+
before do
|
40
|
+
allow(options).to receive(:cached?).and_return(true)
|
41
|
+
expect(counting_store).to receive(:del)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'saves the value' do
|
45
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
46
|
+
expect(relation_object).to receive(:save!)
|
47
|
+
saver.save!
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'when cached? is false' do
|
52
|
+
before do
|
53
|
+
allow(options).to receive(:cached?).and_return(false)
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'when method is passed' do
|
57
|
+
before do
|
58
|
+
allow(options).to receive(:method).and_return("call_this_thing")
|
59
|
+
allow(relation_object).to receive(:call_this_thing).and_return(4)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'saves the value' do
|
63
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
64
|
+
expect(relation_object).to receive(:save!)
|
65
|
+
saver.save!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'when method is not passed' do
|
70
|
+
|
71
|
+
before do
|
72
|
+
allow(Boo).to receive(:table_name).and_return("boos")
|
73
|
+
allow(relation_object).to receive(:boos).and_return(double(count: 4))
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'saves the value' do
|
77
|
+
expect(relation_object).to receive(:boo_count=).with(4)
|
78
|
+
expect(relation_object).to receive(:save!)
|
79
|
+
saver.save!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Counter::Cache::Counters::BufferCounter::Updater do
|
4
|
+
let(:options) { double(relation: "boo", relation_class_name: "Boo", column: "boo", relation_id: nil) }
|
5
|
+
let(:source_object) { double(boo_id: 1) }
|
6
|
+
let(:updater) { Counter::Cache::Counters::BufferCounter::Updater.new(source_object, options, "Hello") }
|
7
|
+
|
8
|
+
describe "#update" do
|
9
|
+
describe "when valid" do
|
10
|
+
it "sends direction and enqueues" do
|
11
|
+
expect(updater).to receive(:valid?) { true }
|
12
|
+
expect(updater).to receive(:decr) { true }
|
13
|
+
expect(updater).to receive(:enqueue) { true }
|
14
|
+
updater.update!(:decr)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "when invalid" do
|
19
|
+
it "sends direction and enqueues" do
|
20
|
+
expect(updater).to receive(:valid?) { false }
|
21
|
+
expect(updater).to receive(:decr).never
|
22
|
+
expect(updater).to receive(:enqueue).never
|
23
|
+
updater.update!(:decr)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#enqueue" do
|
29
|
+
it 'constructs and calls enqueue! on the enqueue' do
|
30
|
+
expect(Counter::Cache::Counters::BufferCounter::Enqueuer).to receive(:new).with(options,
|
31
|
+
source_object.class.name,
|
32
|
+
1,
|
33
|
+
"Boo",
|
34
|
+
"Hello")
|
35
|
+
.and_return(double(enqueue!: true))
|
36
|
+
updater.send(:enqueue)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#decr" do
|
41
|
+
it 'calls decr on the redis instance' do
|
42
|
+
expect_any_instance_of(Counter::Cache::Redis).to receive(:decr)
|
43
|
+
updater.send(:decr)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#incr" do
|
48
|
+
it 'calls decr on the redis instance' do
|
49
|
+
expect_any_instance_of(Counter::Cache::Redis).to receive(:incr)
|
50
|
+
updater.send(:incr)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "valid?" do
|
55
|
+
describe "With no if value" do
|
56
|
+
let(:options) { double(relation: "boo", if_value: nil, relation_id: nil) }
|
57
|
+
|
58
|
+
describe 'with relation_id' do
|
59
|
+
let(:source_object) { double(boo_id: 123) }
|
60
|
+
|
61
|
+
it 'returns true' do
|
62
|
+
expect(updater.send(:valid?)).to eq(true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'without relation_id' do
|
67
|
+
let(:source_object) { double(boo_id: nil) }
|
68
|
+
|
69
|
+
it 'returns false' do
|
70
|
+
expect(updater.send(:valid?)).to eq(false)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "With if object" do
|
76
|
+
let(:source_object) { double(boo_id: 123) }
|
77
|
+
let(:options) { double(relation: "boo", if_value: if_value, relation_id: nil) }
|
78
|
+
|
79
|
+
describe 'if value returns false' do
|
80
|
+
let(:if_value) { double(call: true) }
|
81
|
+
|
82
|
+
it 'returns true' do
|
83
|
+
expect(updater.send(:valid?)).to eq(true)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'if value returns true' do
|
88
|
+
let(:if_value) { double(call: false) }
|
89
|
+
|
90
|
+
it 'returns false' do
|
91
|
+
expect(updater.send(:valid?)).to eq(false)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|