rcgt-sidekiq-limit_fetch 3.4.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +161 -0
  8. data/Rakefile +12 -0
  9. data/bench/compare.rb +52 -0
  10. data/demo/Gemfile +7 -0
  11. data/demo/README.md +37 -0
  12. data/demo/Rakefile +99 -0
  13. data/demo/app/workers/a_worker.rb +8 -0
  14. data/demo/app/workers/b_worker.rb +8 -0
  15. data/demo/app/workers/c_worker.rb +9 -0
  16. data/demo/app/workers/fast_worker.rb +8 -0
  17. data/demo/app/workers/slow_worker.rb +8 -0
  18. data/demo/config/application.rb +11 -0
  19. data/demo/config/boot.rb +2 -0
  20. data/demo/config/environment.rb +2 -0
  21. data/demo/config/environments/development.rb +9 -0
  22. data/lib/sidekiq/extensions/manager.rb +16 -0
  23. data/lib/sidekiq/extensions/queue.rb +23 -0
  24. data/lib/sidekiq/limit_fetch/global/monitor.rb +72 -0
  25. data/lib/sidekiq/limit_fetch/global/selector.rb +125 -0
  26. data/lib/sidekiq/limit_fetch/global/semaphore.rb +175 -0
  27. data/lib/sidekiq/limit_fetch/instances.rb +19 -0
  28. data/lib/sidekiq/limit_fetch/queues.rb +124 -0
  29. data/lib/sidekiq/limit_fetch/unit_of_work.rb +24 -0
  30. data/lib/sidekiq/limit_fetch.rb +58 -0
  31. data/lib/sidekiq-limit_fetch.rb +1 -0
  32. data/rcgt-sidekiq-limit_fetch.gemspec +24 -0
  33. data/spec/sidekiq/extensions/queue_spec.rb +94 -0
  34. data/spec/sidekiq/limit_fetch/global/monitor_spec.rb +33 -0
  35. data/spec/sidekiq/limit_fetch/queues_spec.rb +100 -0
  36. data/spec/sidekiq/limit_fetch/semaphore_spec.rb +63 -0
  37. data/spec/sidekiq/limit_fetch_spec.rb +48 -0
  38. data/spec/spec_helper.rb +28 -0
  39. metadata +149 -0
@@ -0,0 +1,125 @@
1
+ module Sidekiq::LimitFetch::Global
2
+ module Selector
3
+ extend self
4
+
5
+ MUTEX_FOR_UUID = Mutex.new
6
+
7
+ def acquire(queues, namespace)
8
+ redis_eval :acquire, [namespace, uuid, queues]
9
+ end
10
+
11
+ def release(queues, namespace)
12
+ redis_eval :release, [namespace, uuid, queues]
13
+ end
14
+
15
+ def uuid
16
+ # - if we'll remove "@uuid ||=" from inside of mutex
17
+ # then @uuid can be overwritten
18
+ # - if we'll remove "@uuid ||=" from outside of mutex
19
+ # then each read will lead to mutex
20
+ @uuid ||= MUTEX_FOR_UUID.synchronize { @uuid || SecureRandom.uuid }
21
+ end
22
+
23
+ private
24
+
25
+ def redis_eval(script_name, args)
26
+ Sidekiq.redis do |it|
27
+ begin
28
+ it.evalsha send("redis_#{script_name}_sha"), argv: args
29
+ rescue Redis::CommandError => error
30
+ raise unless error.message.include? 'NOSCRIPT'
31
+ it.eval send("redis_#{script_name}_script"), argv: args
32
+ end
33
+ end
34
+ end
35
+
36
+ def redis_acquire_sha
37
+ @acquire_sha ||= Digest::SHA1.hexdigest redis_acquire_script
38
+ end
39
+
40
+ def redis_release_sha
41
+ @release_sha ||= Digest::SHA1.hexdigest redis_release_script
42
+ end
43
+
44
+ def redis_acquire_script
45
+ <<-LUA
46
+ local namespace = table.remove(ARGV, 1)..'limit_fetch:'
47
+ local worker_name = table.remove(ARGV, 1)
48
+ local queues = ARGV
49
+ local available = {}
50
+ local unblocked = {}
51
+ local locks
52
+ local process_locks
53
+ local blocking_mode
54
+
55
+ for _, queue in ipairs(queues) do
56
+ if not blocking_mode or unblocked[queue] then
57
+ local probed_key = namespace..'probed:'..queue
58
+ local pause_key = namespace..'pause:'..queue
59
+ local limit_key = namespace..'limit:'..queue
60
+ local process_limit_key = namespace..'process_limit:'..queue
61
+ local block_key = namespace..'block:'..queue
62
+
63
+ local paused, limit, process_limit, can_block =
64
+ unpack(redis.call('mget',
65
+ pause_key,
66
+ limit_key,
67
+ process_limit_key,
68
+ block_key
69
+ ))
70
+
71
+ if not paused then
72
+ limit = tonumber(limit)
73
+ process_limit = tonumber(process_limit)
74
+
75
+ if can_block or limit then
76
+ locks = redis.call('llen', probed_key)
77
+ end
78
+
79
+ if process_limit then
80
+ local all_locks = redis.call('lrange', probed_key, 0, -1)
81
+ process_locks = 0
82
+ for _, process in ipairs(all_locks) do
83
+ if process == worker_name then
84
+ process_locks = process_locks + 1
85
+ end
86
+ end
87
+ end
88
+
89
+ if not blocking_mode then
90
+ blocking_mode = can_block and locks > 0
91
+ end
92
+
93
+ if blocking_mode and can_block ~= 'true' then
94
+ for unblocked_queue in string.gmatch(can_block, "[^,]+") do
95
+ unblocked[unblocked_queue] = true
96
+ end
97
+ end
98
+
99
+ if (not limit or limit > locks) and
100
+ (not process_limit or process_limit > process_locks) then
101
+ redis.call('rpush', probed_key, worker_name)
102
+ table.insert(available, queue)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ return available
109
+ LUA
110
+ end
111
+
112
+ def redis_release_script
113
+ <<-LUA
114
+ local namespace = table.remove(ARGV, 1)..'limit_fetch:'
115
+ local worker_name = table.remove(ARGV, 1)
116
+ local queues = ARGV
117
+
118
+ for _, queue in ipairs(queues) do
119
+ local probed_key = namespace..'probed:'..queue
120
+ redis.call('lrem', probed_key, 1, worker_name)
121
+ end
122
+ LUA
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,175 @@
1
+ module Sidekiq::LimitFetch::Global
2
+ class Semaphore
3
+ PREFIX = 'limit_fetch'
4
+
5
+ attr_reader :local_busy
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @lock = Mutex.new
10
+ @local_busy = 0
11
+ end
12
+
13
+ def limit
14
+ value = redis {|it| it.get "#{PREFIX}:limit:#@name" }
15
+ value.to_i if value
16
+ end
17
+
18
+ def limit=(value)
19
+ @limit_changed = true
20
+
21
+ if value
22
+ redis {|it| it.set "#{PREFIX}:limit:#@name", value }
23
+ else
24
+ redis {|it| it.del "#{PREFIX}:limit:#@name" }
25
+ end
26
+ end
27
+
28
+ def limit_changed?
29
+ @limit_changed
30
+ end
31
+
32
+ def process_limit
33
+ value = redis {|it| it.get "#{PREFIX}:process_limit:#@name" }
34
+ value.to_i if value
35
+ end
36
+
37
+ def process_limit=(value)
38
+ if value
39
+ redis {|it| it.set "#{PREFIX}:process_limit:#@name", value }
40
+ else
41
+ redis {|it| it.del "#{PREFIX}:process_limit:#@name" }
42
+ end
43
+ end
44
+
45
+ def acquire
46
+ Selector.acquire([@name], namespace).size > 0
47
+ end
48
+
49
+ def release
50
+ redis {|it| it.lrem "#{PREFIX}:probed:#@name", 1, Selector.uuid }
51
+ end
52
+
53
+ def busy
54
+ redis {|it| it.llen "#{PREFIX}:busy:#@name" }
55
+ end
56
+
57
+ def busy_processes
58
+ redis {|it| it.lrange "#{PREFIX}:busy:#@name", 0, -1 }
59
+ end
60
+
61
+ def increase_busy
62
+ increase_local_busy
63
+ redis {|it| it.rpush "#{PREFIX}:busy:#@name", Selector.uuid }
64
+ end
65
+
66
+ def decrease_busy
67
+ decrease_local_busy
68
+ redis {|it| it.lrem "#{PREFIX}:busy:#@name", 1, Selector.uuid }
69
+ end
70
+
71
+ def probed
72
+ redis {|it| it.llen "#{PREFIX}:probed:#@name" }
73
+ end
74
+
75
+ def probed_processes
76
+ redis {|it| it.lrange "#{PREFIX}:probed:#@name", 0, -1 }
77
+ end
78
+
79
+ def pause
80
+ redis {|it| it.set "#{PREFIX}:pause:#@name", true }
81
+ end
82
+
83
+ def pause_for_ms ms
84
+ redis {|it| it.psetex "#{PREFIX}:pause:#@name", ms, true }
85
+ end
86
+
87
+ def unpause
88
+ redis {|it| it.del "#{PREFIX}:pause:#@name" }
89
+ end
90
+
91
+ def paused?
92
+ redis {|it| it.get "#{PREFIX}:pause:#@name" }
93
+ end
94
+
95
+ def block
96
+ redis {|it| it.set "#{PREFIX}:block:#@name", true }
97
+ end
98
+
99
+ def block_except(*queues)
100
+ raise ArgumentError if queues.empty?
101
+ redis {|it| it.set "#{PREFIX}:block:#@name", queues.join(',') }
102
+ end
103
+
104
+ def unblock
105
+ redis {|it| it.del "#{PREFIX}:block:#@name" }
106
+ end
107
+
108
+ def blocking?
109
+ redis {|it| it.get "#{PREFIX}:block:#@name" }
110
+ end
111
+
112
+ def increase_local_busy
113
+ @lock.synchronize { @local_busy += 1 }
114
+ end
115
+
116
+ def decrease_local_busy
117
+ @lock.synchronize { @local_busy -= 1 }
118
+ end
119
+
120
+ def local_busy?
121
+ @local_busy > 0
122
+ end
123
+
124
+ def explain
125
+ <<-END.gsub(/^ {8}/, '')
126
+ Current sidekiq process: #{Selector.uuid}
127
+
128
+ All processes:
129
+ #{Monitor.all_processes.join "\n"}
130
+
131
+ Stale processes:
132
+ #{Monitor.old_processes.join "\n"}
133
+
134
+ Locked queue processes:
135
+ #{probed_processes.sort.join "\n"}
136
+
137
+ Busy queue processes:
138
+ #{busy_processes.sort.join "\n"}
139
+
140
+ Limit:
141
+ #{limit.inspect}
142
+
143
+ Process limit:
144
+ #{process_limit.inspect}
145
+
146
+ Blocking:
147
+ #{blocking?}
148
+ END
149
+ end
150
+
151
+ def remove_locks_except!(processes)
152
+ locked_processes = probed_processes.uniq
153
+ (locked_processes - processes).each do |dead_process|
154
+ remove_lock! dead_process
155
+ end
156
+ end
157
+
158
+ def remove_lock!(process)
159
+ redis do |it|
160
+ it.lrem "#{PREFIX}:probed:#@name", 0, process
161
+ it.lrem "#{PREFIX}:busy:#@name", 0, process
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def redis(&block)
168
+ Sidekiq.redis(&block)
169
+ end
170
+
171
+ def namespace
172
+ Sidekiq::LimitFetch::Queues.namespace
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,19 @@
1
+ module Sidekiq::LimitFetch::Instances
2
+ def self.extended(klass)
3
+ klass.instance_variable_set :@instances, {}
4
+ end
5
+
6
+ def new(*args)
7
+ @instances[args] ||= super
8
+ end
9
+
10
+ alias [] new
11
+
12
+ def instances
13
+ @instances.values
14
+ end
15
+
16
+ def reset_instances!
17
+ @instances = {}
18
+ end
19
+ end
@@ -0,0 +1,124 @@
1
+ module Sidekiq::LimitFetch::Queues
2
+ extend self
3
+
4
+ THREAD_KEY = :acquired_queues
5
+
6
+ def start(options)
7
+ @queues = options[:queues]
8
+ @dynamic = options[:dynamic]
9
+
10
+ @limits = options[:limits] || {}
11
+ @process_limits = options[:process_limits] || {}
12
+ @blocks = options[:blocking] || []
13
+
14
+ options[:strict] ? strict_order! : weighted_order!
15
+
16
+ apply_process_limit_to_queues
17
+ apply_limit_to_queues
18
+ apply_blocks_to_queues
19
+ end
20
+
21
+ def acquire
22
+ selector.acquire(ordered_queues, namespace)
23
+ .tap {|it| save it }
24
+ .map {|it| "queue:#{it}" }
25
+ end
26
+
27
+ def release_except(full_name)
28
+ queues = restore
29
+ queues.delete full_name[/queue:(.*)/, 1] if full_name
30
+
31
+ Sidekiq::LimitFetch.redis_retryable do
32
+ selector.release queues, namespace
33
+ end
34
+ end
35
+
36
+ def dynamic?
37
+ @dynamic
38
+ end
39
+
40
+ def add(queues)
41
+ queues.each do |queue|
42
+ unless @queues.include? queue
43
+ apply_process_limit_to_queue(queue)
44
+ apply_limit_to_queue(queue)
45
+
46
+ @queues.push queue
47
+ end
48
+ end
49
+ end
50
+
51
+ def strict_order!
52
+ @queues.uniq!
53
+ def ordered_queues; @queues end
54
+ end
55
+
56
+ def weighted_order!
57
+ def ordered_queues; @queues.shuffle.uniq end
58
+ end
59
+
60
+ def namespace
61
+ @namespace ||= Sidekiq.redis do |it|
62
+ if it.respond_to?(:namespace) and it.namespace
63
+ "#{it.namespace}:"
64
+ else
65
+ ''
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def apply_process_limit_to_queues
73
+ @queues.uniq.each do |queue_name|
74
+ apply_process_limit_to_queue(queue_name)
75
+ end
76
+ end
77
+
78
+ def apply_process_limit_to_queue(queue_name)
79
+ queue = Sidekiq::Queue[queue_name]
80
+ queue.process_limit = @process_limits[queue_name.to_s] || @process_limits[queue_name.to_sym]
81
+ end
82
+
83
+ def apply_limit_to_queues
84
+ @queues.uniq.each do |queue_name|
85
+ apply_limit_to_queue(queue_name)
86
+ end
87
+ end
88
+
89
+ def apply_limit_to_queue(queue_name)
90
+ queue = Sidekiq::Queue[queue_name]
91
+
92
+ unless queue.limit_changed?
93
+ queue.limit = @limits[queue_name.to_s] || @limits[queue_name.to_sym]
94
+ end
95
+ end
96
+
97
+ def apply_blocks_to_queues
98
+ @queues.uniq.each do |queue_name|
99
+ Sidekiq::Queue[queue_name].unblock
100
+ end
101
+
102
+ @blocks.to_a.each do |it|
103
+ if it.is_a? Array
104
+ it.each {|name| Sidekiq::Queue[name].block_except it }
105
+ else
106
+ Sidekiq::Queue[it].block
107
+ end
108
+ end
109
+ end
110
+
111
+ def selector
112
+ Sidekiq::LimitFetch::Global::Selector
113
+ end
114
+
115
+ def save(queues)
116
+ Thread.current[THREAD_KEY] = queues
117
+ end
118
+
119
+ def restore
120
+ Thread.current[THREAD_KEY] || []
121
+ ensure
122
+ Thread.current[THREAD_KEY] = nil
123
+ end
124
+ end
@@ -0,0 +1,24 @@
1
+ module Sidekiq
2
+ class LimitFetch::UnitOfWork < BasicFetch::UnitOfWork
3
+ def initialize(queue, job)
4
+ super
5
+ redis_retryable { Queue[queue_name].increase_busy }
6
+ end
7
+
8
+ def acknowledge
9
+ redis_retryable { Queue[queue_name].decrease_busy }
10
+ redis_retryable { Queue[queue_name].release }
11
+ end
12
+
13
+ def requeue
14
+ super
15
+ acknowledge
16
+ end
17
+
18
+ private
19
+
20
+ def redis_retryable(&block)
21
+ Sidekiq::LimitFetch.redis_retryable(&block)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ require 'forwardable'
2
+ require 'sidekiq'
3
+ require 'sidekiq/manager'
4
+ require 'sidekiq/api'
5
+
6
+ module Sidekiq::LimitFetch
7
+ autoload :UnitOfWork, 'sidekiq/limit_fetch/unit_of_work'
8
+
9
+ require_relative 'limit_fetch/instances'
10
+ require_relative 'limit_fetch/queues'
11
+ require_relative 'limit_fetch/global/semaphore'
12
+ require_relative 'limit_fetch/global/selector'
13
+ require_relative 'limit_fetch/global/monitor'
14
+ require_relative 'extensions/queue'
15
+ require_relative 'extensions/manager'
16
+
17
+ extend self
18
+
19
+ def new(_)
20
+ self
21
+ end
22
+
23
+ def retrieve_work
24
+ queue, job = redis_brpop(Queues.acquire)
25
+ Queues.release_except(queue)
26
+ UnitOfWork.new(queue, job) if job
27
+ end
28
+
29
+ # Backwards compatibility for sidekiq v6.1.0
30
+ # @see https://github.com/mperham/sidekiq/pull/4602
31
+ def bulk_requeue(*args)
32
+ if Sidekiq::BasicFetch.respond_to?(:bulk_requeue) # < 6.1.0
33
+ Sidekiq::BasicFetch.bulk_requeue(*args)
34
+ else # 6.1.0+
35
+ Sidekiq::BasicFetch.new(Sidekiq.options).bulk_requeue(*args)
36
+ end
37
+ end
38
+
39
+ def redis_retryable
40
+ yield
41
+ rescue Redis::BaseConnectionError
42
+ sleep 1
43
+ retry
44
+ end
45
+
46
+ private
47
+
48
+ TIMEOUT = Sidekiq::BasicFetch::TIMEOUT
49
+
50
+ def redis_brpop(queues)
51
+ if queues.empty?
52
+ sleep TIMEOUT # there are no queues to handle, so lets sleep
53
+ [] # and return nothing
54
+ else
55
+ redis_retryable { Sidekiq.redis { |it| it.brpop *queues, TIMEOUT } }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1 @@
1
+ require_relative 'sidekiq/limit_fetch'
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'rcgt-sidekiq-limit_fetch'
3
+ gem.version = '3.4.1'
4
+ gem.license = 'MIT'
5
+ gem.authors = 'RCGT Consulting Inc.'
6
+ gem.email = 'kumanan.yogaratnam@rcgtconsulting.com'
7
+ gem.summary = 'Sidekiq strategy to support queue limits'
8
+ gem.homepage = 'https://github.com/RCGTConsulting/sidekiq-limit_fetch'
9
+ gem.description = <<-DESCRIPTION
10
+ A fork of https://github.com/brainopia/sidekiq-limit_fetch
11
+
12
+ Sidekiq strategy to restrict number of workers
13
+ which are able to run specified queues simultaneously.
14
+ DESCRIPTION
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.test_files = gem.files.grep %r{^spec/}
18
+ gem.require_paths = %w(lib)
19
+
20
+ gem.add_dependency 'sidekiq', '>= 4'
21
+ gem.add_development_dependency 'redis-namespace', '~> 1.5', '>= 1.5.2'
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'rake'
24
+ end
@@ -0,0 +1,94 @@
1
+ RSpec.describe Sidekiq::Queue do
2
+ context 'singleton' do
3
+ shared_examples :constructor do
4
+ it 'with default name' do
5
+ new_object = -> { described_class.send constructor }
6
+ expect(new_object.call).to eq new_object.call
7
+ end
8
+
9
+ it 'with given name' do
10
+ new_object = ->(name) { described_class.send constructor, name }
11
+ expect(new_object.call('name')).to eq new_object.call('name')
12
+ end
13
+ end
14
+
15
+ context '.new' do
16
+ let(:constructor) { :new }
17
+ it_behaves_like :constructor
18
+ end
19
+
20
+ context '.[]' do
21
+ let(:constructor) { :[] }
22
+ it_behaves_like :constructor
23
+ end
24
+
25
+ context '#lock' do
26
+ let(:name) { 'example' }
27
+ let(:queue) { Sidekiq::Queue[name] }
28
+
29
+ it 'should be available' do
30
+ expect(queue.acquire).to be
31
+ end
32
+
33
+ it 'should be pausable' do
34
+ queue.pause
35
+ expect(queue.acquire).not_to be
36
+ end
37
+
38
+ it 'should be continuable' do
39
+ queue.pause
40
+ queue.unpause
41
+ expect(queue.acquire).to be
42
+ end
43
+
44
+ it 'should be limitable' do
45
+ queue.limit = 1
46
+ expect(queue.acquire).to be
47
+ expect(queue.acquire).not_to be
48
+ end
49
+
50
+ it 'should be resizable' do
51
+ queue.limit = 0
52
+ expect(queue.acquire).not_to be
53
+ queue.limit = nil
54
+ expect(queue.acquire).to be
55
+ end
56
+
57
+ it 'should be countable' do
58
+ queue.limit = 3
59
+ 5.times { queue.acquire }
60
+ expect(queue.probed).to eq 3
61
+ end
62
+
63
+ it 'should be releasable' do
64
+ queue.acquire
65
+ expect(queue.probed).to eq 1
66
+ queue.release
67
+ expect(queue.probed).to eq 0
68
+ end
69
+
70
+ it 'should tell if paused' do
71
+ expect(queue).not_to be_paused
72
+ queue.pause
73
+ expect(queue).to be_paused
74
+ queue.unpause
75
+ expect(queue).not_to be_paused
76
+ end
77
+
78
+ it 'should tell if blocking' do
79
+ expect(queue).not_to be_blocking
80
+ queue.block
81
+ expect(queue).to be_blocking
82
+ queue.unblock
83
+ expect(queue).not_to be_blocking
84
+ end
85
+
86
+ it 'should be marked as changed' do
87
+ queue = Sidekiq::Queue["uniq_#{name}"]
88
+ expect(queue).not_to be_limit_changed
89
+ queue.limit = 3
90
+ expect(queue).to be_limit_changed
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe Sidekiq::LimitFetch::Global::Monitor do
2
+ let(:monitor) { described_class.start! ttl, timeout }
3
+ let(:ttl) { 1 }
4
+ let(:queue) { Sidekiq::Queue[name] }
5
+ let(:name) { 'default' }
6
+
7
+ before { monitor }
8
+ after { monitor.kill }
9
+
10
+ context 'old locks' do
11
+ let(:timeout) { 0.5 }
12
+
13
+ it 'should remove invalidated old locks' do
14
+ 2.times { queue.acquire }
15
+ sleep 2*ttl
16
+ expect(queue.probed).to eq 2
17
+
18
+ allow(described_class).to receive(:update_heartbeat)
19
+ sleep 2*ttl
20
+ expect(queue.probed).to eq 0
21
+ end
22
+
23
+ it 'should remove invalid locks' do
24
+ 2.times { queue.acquire }
25
+ allow(described_class).to receive(:update_heartbeat)
26
+ Sidekiq.redis do |it|
27
+ it.del Sidekiq::LimitFetch::Global::Monitor::PROCESS_SET
28
+ end
29
+ sleep 2*ttl
30
+ expect(queue.probed).to eq 0
31
+ end
32
+ end
33
+ end