shoryuken 2.1.3 → 3.0.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -0
  3. data/.rubocop.yml +8 -2
  4. data/.travis.yml +1 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +20 -104
  7. data/Rakefile +0 -1
  8. data/bin/cli/base.rb +42 -0
  9. data/bin/cli/sqs.rb +188 -0
  10. data/bin/shoryuken +47 -9
  11. data/examples/default_worker.rb +1 -1
  12. data/lib/shoryuken.rb +75 -55
  13. data/lib/shoryuken/client.rb +3 -15
  14. data/lib/shoryuken/default_worker_registry.rb +9 -5
  15. data/lib/shoryuken/environment_loader.rb +9 -40
  16. data/lib/shoryuken/fetcher.rb +16 -18
  17. data/lib/shoryuken/launcher.rb +5 -28
  18. data/lib/shoryuken/manager.rb +60 -140
  19. data/lib/shoryuken/message.rb +4 -13
  20. data/lib/shoryuken/middleware/chain.rb +1 -18
  21. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +7 -16
  22. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +25 -21
  23. data/lib/shoryuken/polling.rb +2 -4
  24. data/lib/shoryuken/processor.rb +2 -11
  25. data/lib/shoryuken/queue.rb +1 -3
  26. data/lib/shoryuken/runner.rb +143 -0
  27. data/lib/shoryuken/util.rb +0 -8
  28. data/lib/shoryuken/version.rb +1 -1
  29. data/lib/shoryuken/worker.rb +1 -1
  30. data/shoryuken.gemspec +6 -5
  31. data/spec/integration/launcher_spec.rb +4 -3
  32. data/spec/shoryuken/client_spec.rb +2 -45
  33. data/spec/shoryuken/default_worker_registry_spec.rb +12 -10
  34. data/spec/shoryuken/environment_loader_spec.rb +34 -0
  35. data/spec/shoryuken/manager_spec.rb +11 -21
  36. data/spec/shoryuken/middleware/chain_spec.rb +0 -24
  37. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +0 -2
  38. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +46 -29
  39. data/spec/shoryuken/processor_spec.rb +5 -5
  40. data/spec/shoryuken/{cli_spec.rb → runner_spec.rb} +8 -22
  41. data/spec/shoryuken_spec.rb +13 -1
  42. data/spec/spec_helper.rb +3 -8
  43. metadata +29 -22
  44. data/lib/shoryuken/aws_config.rb +0 -64
  45. data/lib/shoryuken/cli.rb +0 -215
  46. data/lib/shoryuken/sns_arn.rb +0 -27
  47. data/lib/shoryuken/topic.rb +0 -17
  48. data/spec/shoryuken/sns_arn_spec.rb +0 -42
  49. data/spec/shoryuken/topic_spec.rb +0 -32
  50. data/spec/shoryuken_endpoint.yml +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 84439f7b9ae680a0363a63e98c6cbba645541b86
4
- data.tar.gz: 72e3f662b660f594d78bb4328b137c43774eaf9d
3
+ metadata.gz: 3810f509ebbedaf23dfcd756b17f487097d99ab7
4
+ data.tar.gz: 2dabc0be8dbab5536377b7fe6cb6e371b1c0a71c
5
5
  SHA512:
6
- metadata.gz: 2c50f7e305327dc7e155c84a8e520579b3d72f0e5d0d224d6b9a0e346a6923c5348e66bd1c63e3a3cbd9ed22adc488680194266d80004556670250c7cc8db450
7
- data.tar.gz: c0154a86dd1098ff213c55411fb6fcc50b6a5f2eebb5d207443cce9952355ed5f2ab191f5d321ac91322bb2f1e44b3099404d7b0018aaf300d450f87450d85c2
6
+ metadata.gz: 5fe6096c4df914e64b56dcbcad889bda4827cbf6882ab018d4e8568cab9caf61f548a6117ba3b5607da13abcad049be58c59d8498d76abcc0b6f366579eaa79d
7
+ data.tar.gz: 61c52ec5a273c3294679baff1b00f5938d1aec218f0a304ff7503c2c28f537407306380e8fce9e954ba4caa92113e682289a68de42dd26e3d5515f9714e30d92
data/.codeclimate.yml CHANGED
@@ -11,6 +11,8 @@ engines:
11
11
  enabled: true
12
12
  rubocop:
13
13
  enabled: true
14
+ config:
15
+ file: .rubocop.yml
14
16
  ratings:
15
17
  paths:
16
18
  - "**.rb"
data/.rubocop.yml CHANGED
@@ -1,9 +1,12 @@
1
- LineLength:
2
- Max: 120
1
+ AllCops:
2
+ TargetRubyVersion: 2.0
3
3
 
4
4
  Style/SignalException:
5
5
  Enabled: false
6
6
 
7
+ Style/DoubleNegation:
8
+ Enabled: false
9
+
7
10
  Style/SpaceAroundEqualsInParameterDefault:
8
11
  Enabled: false
9
12
 
@@ -28,6 +31,9 @@ Metrics/ClassLength:
28
31
  Metrics/ParameterLists:
29
32
  Enabled: false
30
33
 
34
+ Metrics/LineLength:
35
+ Max: 120
36
+
31
37
  Metrics/MethodLength :
32
38
  Enabled: false
33
39
 
data/.travis.yml CHANGED
@@ -3,6 +3,7 @@ rvm:
3
3
  - 2.0.0
4
4
  - 2.1.0
5
5
  - 2.2.0
6
+ - 2.3.3
6
7
 
7
8
  notifications:
8
9
  email:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [v3.0.0] - 2017-03-12
2
+ - Replace Celluloid with Concurrent Ruby
3
+ - [#291](https://github.com/phstc/shoryuken/pull/291)
4
+
5
+ - Remove AWS configuration from Shoryuken. Now AWS should be configured from outside. Check [this](https://github.com/phstc/shoryuken/wiki/Configure-the-AWS-Client) for more details
6
+ - [#317](https://github.com/phstc/shoryuken/pull/291)
7
+
8
+ - Remove deprecation warnings
9
+ - [#326](https://github.com/phstc/shoryuken/pull/326)
10
+
11
+ - Allow dynamic adding queues
12
+ - [#322](https://github.com/phstc/shoryuken/pull/326)
13
+
14
+ - Support retry_intervals passed in as a lambda. Auto coerce intervals into integer
15
+ - [#329](https://github.com/phstc/shoryuken/pull/329)
16
+
17
+ - Add SQS commands `shoryuken help sqs`, such `ls`, `mv`, `dump` and `requeue`
18
+ - [#330](https://github.com/phstc/shoryuken/pull/330)
19
+
1
20
  ## [v2.1.3] - 2017-01-27
2
21
  - Show a warn message when batch isn't supported
3
22
  - [#302](https://github.com/phstc/shoryuken/pull/302)
data/README.md CHANGED
@@ -20,18 +20,18 @@ concurrency: 25
20
20
  delay: 25
21
21
  queues:
22
22
  - [high_priority, 6]
23
- - [default, 2]
23
+ - [normal_priority, 2]
24
24
  - [low_priority, 1]
25
25
  ```
26
26
 
27
- And supposing all the queues are full of messages, the configuration above will make Shoryuken to process `high_priority` 3 times more than `default` and 6 times more than `low_priority`,
28
- splitting the work among the `concurrency: 25` available processors.
27
+ And supposing all the queues are full of messages, the configuration above will make Shoryuken to process `high_priority` 3 times more than `normal_priority` and 6 times more than `low_priority`,
28
+ splitting the work load among all available processors `concurrency: 25` .
29
29
 
30
- If `high_priority` gets empty, Shoryuken will keep using the 25 processors, but only to process `default` (2 times more than `low_priority`) and `low_priority`.
30
+ If `high_priority` gets empty, Shoryuken will keep using the 25 processors, but only to process `normal_priority` and `low_priority`.
31
31
 
32
- If `high_priority` receives a new message, Shoryuken will smoothly increase back the `high_priority` weight one by one until it reaches the weight of 6 again, which is the maximum configured for `high_priority`.
32
+ If `high_priority` receives a new message, Shoryuken will smoothly increase back its weight one by one until it reaches the weight of 6 again.
33
33
 
34
- If all queues get empty, all processors will be changed to the waiting state and the queues will be checked every `delay: 25`. If any queue receives a new message, Shoryuken will start processing again. [Check the delay option documentation for more information](https://github.com/phstc/shoryuken/wiki/Shoryuken-options#delay).
34
+ [If a queue gets empty, Shoryuken will pause checking it for `delay: 25`](https://github.com/phstc/shoryuken/wiki/Shoryuken-options#delay).
35
35
 
36
36
 
37
37
  ### Fetch in batches
@@ -40,7 +40,7 @@ To be even more performant and cost effective, Shoryuken fetches SQS messages in
40
40
 
41
41
  ## Requirements
42
42
 
43
- Ruby 2.0 or greater. Ruby 1.9 is no longer supported.
43
+ Ruby 2.0 or greater.
44
44
 
45
45
  ## Installation
46
46
 
@@ -105,7 +105,7 @@ end
105
105
 
106
106
  [Check the Middleware documentation](https://github.com/phstc/shoryuken/wiki/Middleware).
107
107
 
108
- ### Configuration (worker side)
108
+ ### Shoryuken Configuration
109
109
 
110
110
  Sample configuration file `shoryuken.yml`.
111
111
 
@@ -114,87 +114,13 @@ concurrency: 25 # The number of allocated threads to process messages. Default
114
114
  delay: 25 # The delay in seconds to pause a queue when it's empty. Default 0
115
115
  queues:
116
116
  - [high_priority, 6]
117
- - [default, 2]
117
+ - [normal_priority, 2]
118
118
  - [low_priority, 1]
119
119
  ```
120
120
 
121
- And setup ```aws``` options to use ```configure_client``` in `config/initializers/shoryuken.rb`:
121
+ #### AWS Configuration
122
122
 
123
- ```ruby
124
- Shoryuken.configure_client do |config|
125
- config.aws = {
126
- secret_access_key: ..., # or ENV["AWS_SECRET_ACCESS_KEY"]
127
- access_key_id: ..., # or ENV["AWS_ACCESS_KEY_ID"]
128
- region: "us-east-1", # or ENV["AWS_REGION"]
129
- receive_message: { # See http://docs.aws.amazon.com/sdkforruby/api/Aws/SQS/Client.html#receive_message-instance_method
130
- # wait_time_seconds: N, # The number of seconds to wait for new messages when polling. Defaults to the #wait_time_seconds defined on the queue
131
- attribute_names: [
132
- "ApproximateReceiveCount",
133
- "SentTimestamp"
134
- ]
135
- }
136
- }
137
- end
138
- ```
139
-
140
- If you use Shoryuken with plain ruby worker class (not Rails), please call `configure_client` at the beginning of the worker file:
141
-
142
- ```ruby
143
- Shoryuken.configure_client do |config|
144
- config.aws = {
145
- secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
146
- access_key_id: ENV["AWS_ACCESS_KEY_ID"],
147
- region: ENV["AWS_REGION"]
148
- }
149
- end
150
-
151
- class MyWorker
152
- end
153
- ```
154
-
155
- The `aws` section is used to configure both the Aws objects used by Shoryuken internally, and also to set up some Shoryuken-specific config. The Shoryuken-specific keys are listed below, and you can expect any other key defined in that block to be passed on untouched to `Aws::SQS::Client#initialize`:
156
-
157
- - `account_id` is used when generating SNS ARNs
158
- - `sns_endpoint` can be used to explicitly override the SNS endpoint
159
- - `sqs_endpoint` can be used to explicitly override the SQS endpoint
160
- - `receive_message` can be used to define the options passed to the http://docs.aws.amazon.com/sdkforruby/api/Aws/SQS/Client.html#receive_message-instance_method
161
-
162
- The `sns_endpoint` and `sqs_endpoint` Shoryuken-specific options will also fallback to the environment variables `AWS_SNS_ENDPOINT` and `AWS_SQS_ENDPOINT` respectively, if they are set.
163
-
164
- ### Configuration (producer side)
165
-
166
- 'Producer' processes need permissions to put messages into SQS. There are a few ways:
167
-
168
- * Use the `configure_server` in Rails initializer
169
- * Ensure the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` env vars are set.
170
- * Create a `~/.aws/credentials` file.
171
- * Set `Aws.config[:credentials]` from Ruby code (e.g. in a Rails initializer)
172
- * Use the Instance Profiles feature. The IAM role of the targeted machine must have an adequate SQS Policy.
173
-
174
- For example, use the `configure_server` in `config/initializers/shoryuken.rb`:
175
-
176
- ```ruby
177
- Shoryuken.configure_client do |config|
178
- config.aws = {
179
- secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
180
- access_key_id: ENV["AWS_ACCESS_KEY_ID"],
181
- region: ENV["AWS_REGION"]
182
- }
183
- end
184
-
185
- Shoryuken.configure_server do |config|
186
- config.aws = {
187
- secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
188
- access_key_id: ENV["AWS_ACCESS_KEY_ID"],
189
- region: ENV["AWS_REGION"]
190
- }
191
- end
192
- ```
193
-
194
-
195
- Note that storing your credentials into Amazon instances represents a security risk. Instance Profiles tends to be the best choice.
196
-
197
- You can read about these in more detail [here](http://docs.aws.amazon.com/sdkforruby/api/Aws/SQS/Client.html).
123
+ [Check the Configure AWS Client documentation](https://github.com/phstc/shoryuken/wiki/Configure-the-AWS-Client)
198
124
 
199
125
  ### Rails Integration
200
126
 
@@ -206,26 +132,16 @@ You can read about these in more detail [here](http://docs.aws.amazon.com/sdkfor
206
132
  bundle exec shoryuken -r worker.rb -C shoryuken.yml
207
133
  ```
208
134
 
209
- Other options:
210
-
211
- ```bash
212
- shoryuken --help
213
-
214
- shoryuken [options]
215
- -c, --concurrency INT Processor threads to use
216
- -d, --daemon Daemonize process
217
- -q, --queue QUEUE[,WEIGHT]... Queues to process with optional weights
218
- -r, --require [PATH|DIR] Location of the worker
219
- -C, --config PATH Path to YAML config file
220
- -R, --rails Attempts to load the containing Rails project
221
- -L, --logfile PATH Path to writable logfile
222
- -P, --pidfile PATH Path to pidfile
223
- -v, --verbose Print more verbose output
224
- -V, --version Print version and exit
225
- -h, --help Show help
226
- ...
227
- ```
135
+ For other options check `bundle exec shoryuken help start`
136
+
137
+ #### SQS commands
138
+
139
+ Check also some available SQS commands `bundle exec shoryuken help sqs`, such as:
228
140
 
141
+ - `ls` list queues
142
+ - `mv` move messages from one queue to another
143
+ - `dump` dump messages from a queue into a JSON lines file
144
+ - `requeue` requeue messages from a dump file
229
145
 
230
146
  ## More Information
231
147
 
data/Rakefile CHANGED
@@ -10,7 +10,6 @@ end
10
10
  desc 'Open Shoryuken pry console'
11
11
  task :console do
12
12
  require 'pry'
13
- require 'celluloid/current'
14
13
  require 'shoryuken'
15
14
 
16
15
  config_file = File.join File.expand_path('..', __FILE__), 'shoryuken.yml'
data/bin/cli/base.rb ADDED
@@ -0,0 +1,42 @@
1
+ # rubocop:disable Metrics/BlockLength
2
+ module Shoryuken
3
+ module CLI
4
+ class Base < Thor
5
+ no_commands do
6
+ def print_table(entries)
7
+ column_sizes = print_columns_size(entries)
8
+
9
+ entries.map do |entry|
10
+ puts entry.map.with_index { |e, i| print_format_column(e, column_sizes[i]) }.join
11
+ end
12
+ end
13
+
14
+ def print_columns_size(entries)
15
+ column_sizes = Hash.new(0)
16
+
17
+ entries.each do |entry|
18
+ entry.each_with_index do |e, i|
19
+ e = e.to_s
20
+ column_sizes[i] = e.size if column_sizes[i] < e.size
21
+ end
22
+ end
23
+
24
+ column_sizes
25
+ end
26
+
27
+ def print_format_column(column, size)
28
+ size = 40 if size > 40
29
+ size_with_padding = size + 4
30
+ column = column.to_s.ljust(size_with_padding)
31
+ column = "#{column[0...size - 2]}.." if column.size > size_with_padding
32
+ column
33
+ end
34
+
35
+ def fail_task(msg, quit = true)
36
+ say "[FAIL] #{msg}", :red
37
+ exit(1) if quit
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/bin/cli/sqs.rb ADDED
@@ -0,0 +1,188 @@
1
+ require 'date'
2
+
3
+ # rubocop:disable Metrics/AbcSize, Metrics/BlockLength
4
+ module Shoryuken
5
+ module CLI
6
+ class SQS < Base
7
+ namespace :sqs
8
+
9
+ no_commands do
10
+ def normalize_dump_message(message)
11
+ message[:id] = message.delete(:message_id)
12
+ message[:message_body] = message.delete(:body)
13
+ message.delete(:receipt_handle)
14
+ message.delete(:md5_of_body)
15
+ message.delete(:md5_of_message_attributes)
16
+ message
17
+ end
18
+
19
+ def sqs
20
+ @_sqs ||= Aws::SQS::Client.new
21
+ end
22
+
23
+ def find_queue_url(queue_name_prefix)
24
+ urls = sqs.list_queues(queue_name_prefix: queue_name_prefix).queue_urls
25
+
26
+ if urls.size > 1
27
+ fail_task "There's more than one queue starting with #{queue_name_prefix}: #{urls.join(', ')}"
28
+ end
29
+
30
+ url = urls.first
31
+
32
+ fail_task "Queue #{queue_name_prefix} not found" unless url
33
+
34
+ url
35
+ end
36
+
37
+ def batch_delete(url, messages)
38
+ messages.to_a.flatten.each_slice(10) do |batch|
39
+ sqs.delete_message_batch(
40
+ queue_url: url,
41
+ entries: batch.map { |message| { id: message.message_id, receipt_handle: message.receipt_handle } }
42
+ ).failed.any? do |failure|
43
+ say "Could not delete #{failure.id}, code: #{failure.code}", :yellow
44
+ end
45
+ end
46
+ end
47
+
48
+ def batch_send(url, messages)
49
+ messages.to_a.flatten.map(&method(:normalize_dump_message)).each_slice(10) do |batch|
50
+ sqs.send_message_batch(queue_url: url, entries: batch).failed.any? do |failure|
51
+ say "Could not requeue #{failure.id}, code: #{failure.code}", :yellow
52
+ end
53
+ end
54
+ end
55
+
56
+ def find_all(url, limit, &block)
57
+ count = 0
58
+ batch_size = limit > 10 ? 10 : limit
59
+
60
+ loop do
61
+ n = limit - count
62
+ batch_size = n if n < batch_size
63
+
64
+ messages = sqs.receive_message(
65
+ queue_url: url,
66
+ max_number_of_messages: batch_size,
67
+ message_attribute_names: ['All']
68
+ ).messages
69
+
70
+ messages.each { |m| yield m }
71
+
72
+ count += messages.size
73
+
74
+ break if count >= limit
75
+ break if messages.empty?
76
+ end
77
+
78
+ count
79
+ end
80
+
81
+ def list_and_print_queues(urls)
82
+ attrs = %w(QueueArn ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible LastModifiedTimestamp)
83
+
84
+ entries = urls.map { |u| sqs.get_queue_attributes(queue_url: u, attribute_names: attrs).attributes }.map do |q|
85
+ [
86
+ q['QueueArn'].split(':').last,
87
+ q['ApproximateNumberOfMessages'],
88
+ q['ApproximateNumberOfMessagesNotVisible'],
89
+ Time.at(q['LastModifiedTimestamp'].to_i)
90
+ ]
91
+ end
92
+
93
+ entries.unshift(['Queue', 'Messages Available', 'Messages Inflight', 'Last Modified'])
94
+
95
+ print_table(entries)
96
+ end
97
+
98
+ def dump_file(path, queue_name)
99
+ File.join(path, "#{queue_name}-#{Date.today}.jsonl")
100
+ end
101
+ end
102
+
103
+ desc 'ls [QUEUE-NAME-PREFIX]', 'List queues'
104
+ method_option :watch, aliases: '-w', type: :boolean, desc: 'watch queues'
105
+ method_option :watch_interval, type: :numeric, default: 10, desc: 'watch interval'
106
+ def ls(queue_name_prefix = '')
107
+ trap('SIGINT', 'EXIT') # expect ctrl-c from loop
108
+
109
+ urls = sqs.list_queues(queue_name_prefix: queue_name_prefix).queue_urls
110
+
111
+ loop do
112
+ list_and_print_queues(urls)
113
+
114
+ break unless options.watch
115
+
116
+ sleep options.watch_interval
117
+ puts
118
+ end
119
+ end
120
+
121
+ desc 'dump QUEUE-NAME', 'Dump messages from a queue into a JSON lines file'
122
+ method_option :number, aliases: '-n', type: :numeric, default: Float::INFINITY, desc: 'number of messages to dump'
123
+ method_option :path, aliases: '-p', type: :string, default: './', desc: 'path to save the dump file'
124
+ method_option :delete, aliases: '-d', type: :boolean, default: true, desc: 'delete from the queue'
125
+ def dump(queue_name)
126
+ path = dump_file(options.path, queue_name)
127
+
128
+ fail_task "File #{path} already exists" if File.exist?(path)
129
+
130
+ url = find_queue_url(queue_name)
131
+
132
+ messages = []
133
+
134
+ file = nil
135
+
136
+ count = find_all(url, options.number) do |m|
137
+ file ||= File.open(path, 'w')
138
+
139
+ file.puts(JSON.dump(m.to_h))
140
+
141
+ messages << m if options.delete
142
+ end
143
+
144
+ batch_delete(url, messages) if options.delete
145
+
146
+ if count.zero?
147
+ say "Queue #{queue_name} is empty", :yellow
148
+ else
149
+ say "Dump saved in #{path} with #{count} messages", :green
150
+ end
151
+ ensure
152
+ file.close if file
153
+ end
154
+
155
+ desc 'requeue QUEUE-NAME PATH', 'Requeue messages from a dump file'
156
+ def requeue(queue_name, path)
157
+ fail_task "Path #{path} not found" unless File.exist?(path)
158
+
159
+ messages = File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
160
+
161
+ batch_send(find_queue_url(queue_name), messages)
162
+
163
+ say "Requeued #{messages.size} messages from #{path} to #{queue_name}", :green
164
+ end
165
+
166
+ desc 'mv QUEUE-NAME-SOURCE QUEUE-NAME-TARGET', 'Move messages from one queue (source) to another (target)'
167
+ method_option :number, aliases: '-n', type: :numeric, default: Float::INFINITY, desc: 'number of messages to move'
168
+ method_option :delete, aliases: '-d', type: :boolean, default: true, desc: 'delete from the queue'
169
+ def mv(queue_name_source, queue_name_target)
170
+ url_source = find_queue_url(queue_name_source)
171
+ messages = []
172
+
173
+ count = find_all(url_source, options.number) do |m|
174
+ messages << m
175
+ end
176
+
177
+ batch_send(find_queue_url(queue_name_target), messages.map(&:to_h))
178
+ batch_delete(url_source, messages) if options.delete
179
+
180
+ if count.zero?
181
+ say "Queue #{queue_name_source} is empty", :yellow
182
+ else
183
+ say "Moved #{count} messages from #{queue_name_source} to #{queue_name_target}", :green
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end