brianjlandau-resque-scheduler 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg
2
+ nbproject
3
+ .rvmrc
data/HISTORY.md ADDED
@@ -0,0 +1,78 @@
1
+ ## 1.9.6 (2010-10-08)
2
+
3
+ * Support for custom job classes (like resque-status) (mattetti)
4
+
5
+ ## 1.9.5 (2010-09-09)
6
+
7
+ * Updated scheduler rake task to allow for an alternate setup task
8
+ to avoid loading the entire stack. (chewbranca)
9
+ * Fixed sig issue on win32 (#25)
10
+
11
+ ## 1.9.4 (2010-07-29)
12
+
13
+ * Adding ability to remove jobs from delayed queue (joshsz)
14
+ * Fixing issue #23 (removing .present? reference)
15
+
16
+ ## 1.9.3 (2010-07-07)
17
+
18
+ * Bug fix (#19)
19
+
20
+ ## 1.9.2 (2010-06-16)
21
+
22
+ * Fixing issue with redis gem 2.0.1 and redis server 1.2.6 (dbackeus)
23
+
24
+ ## 1.9.1 (2010-06-04)
25
+
26
+ * Fixing issue with redis server 1.2.6 and redis gem 2.0.1
27
+
28
+ ## 1.9.0 (2010-06-04)
29
+
30
+ * Adding redis 2.0 support (bpo)
31
+
32
+ ## 1.8.2 (2010-06-04)
33
+
34
+ * Adding queue now functionality to delayed timestamps (daviddoan)
35
+
36
+ ## 1.8.1 (2010-05-19)
37
+
38
+ * Adding rails_env for scheduled jobs to support scoping jobs by
39
+ RAILS_ENV (gravis).
40
+ * Fixing ruby 1.8.6 compatibility issue.
41
+ * Adding gemspec for bundler support.
42
+
43
+ ## 1.8.0 (2010-04-14)
44
+
45
+ * Moving version to match corresponding resque version
46
+ * Sorting schedule on Scheduler tab
47
+ * Adding tests for resque-web (gravis)
48
+
49
+ ## 1.0.5 (2010-03-01)
50
+
51
+ * Fixed support for overriding queue from schedule config.
52
+ * Removed resque-web dependency on loading the job classes for "Queue Now",
53
+ provided "queue" is specified in the schedule.
54
+ * The queue is now stored with the job and arguments in the delayed queue so
55
+ there is no longer a need for the scheduler to load job classes to introspect
56
+ the queue.
57
+
58
+ ## 1.0.4 (2010-02-26)
59
+
60
+ * Added support for specifying the queue to put the job onto. This allows for
61
+ you to have one job that can go onto multiple queues and be able to schedule
62
+ jobs without having to load the job classes.
63
+
64
+ ## 1.0.3 (2010-02-11)
65
+
66
+ * Added support for scheduled jobs with empty crons. This is helpful to have
67
+ jobs that you don't want on a schedule, but do want to be able to queue by
68
+ clicking a button.
69
+
70
+ ## 1.0.2 (2010-02-?)
71
+
72
+ * Change Delayed Job tab to display job details if only 1 job exists
73
+ for a given timestamp
74
+
75
+ ## 1.0.1 (2010-01-?)
76
+
77
+ * Bugfix: delayed jobs close together resulted in a 5 second sleep
78
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Ben VandenBos
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.markdown ADDED
@@ -0,0 +1,270 @@
1
+ resque-scheduler
2
+ ===============
3
+
4
+ Resque-scheduler is an extension to [Resque](http://github.com/defunkt/resque)
5
+ that adds support for queueing items in the future.
6
+
7
+ Requires redis >=1.1.
8
+
9
+
10
+ Job scheduling is supported in two different way:
11
+
12
+ ### Recurring (scheduled)
13
+
14
+ Recurring (or scheduled) jobs are logically no different than a standard cron
15
+ job. They are jobs that run based on a fixed schedule which is set at startup.
16
+
17
+ The schedule is a list of Resque worker classes with arguments and a
18
+ schedule frequency (in crontab syntax). The schedule is just a hash, but
19
+ is most likely stored in a YAML like so:
20
+
21
+ queue_documents_for_indexing:
22
+ cron: "0 0 * * *"
23
+ class: QueueDocuments
24
+ args:
25
+ description: "This job queues all content for indexing in solr"
26
+
27
+ clear_leaderboards_contributors:
28
+ cron: "30 6 * * 1"
29
+ class: ClearLeaderboards
30
+ args: contributors
31
+ description: "This job resets the weekly leaderboard for contributions"
32
+
33
+ A queue option can also be specified. When job will go onto the specified queue
34
+ if it is available (Even if @queue is specified in the job class). When the
35
+ queue is given it is not necessary for the scheduler to load the class.
36
+
37
+ clear_leaderboards_moderator:
38
+ cron: "30 6 * * 1"
39
+ class: ClearLeaderboards
40
+ queue: scoring
41
+ args: moderators
42
+ description: "This job resets the weekly leaderboard for moderators"
43
+
44
+ And then set the schedule wherever you configure Resque, like so:
45
+
46
+ require 'resque_scheduler'
47
+ Resque.schedule = YAML.load_file(File.join(File.dirname(__FILE__), '../resque_schedule.yml'))
48
+
49
+ Keep in mind, scheduled jobs behave like crons: if your scheduler process (more
50
+ on that later) is not running when a particular job is supposed to be queued,
51
+ it will NOT be ran later when the scheduler process is started back up. In that
52
+ sense, you can sort of think of the scheduler process as crond. Delayed jobs,
53
+ however, are different.
54
+
55
+ A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler)
56
+ for handling the heavy lifting of the actual scheduling engine.
57
+
58
+ ### Delayed jobs
59
+
60
+ Delayed jobs are one-off jobs that you want to be put into a queue at some point
61
+ in the future. The classic example is sending email:
62
+
63
+ Resque.enqueue_at(5.days.from_now, SendFollowUpEmail, :user_id => current_user.id)
64
+
65
+ This will store the job for 5 days in the resque delayed queue at which time the
66
+ scheduler process will pull it from the delayed queue and put it in the
67
+ appropriate work queue for the given job and it will be processed as soon as
68
+ a worker is available.
69
+
70
+ NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
71
+ time is in the past, the job moves from the delayed queue to the actual resque
72
+ work queue and will be completed as workers as free to process it.
73
+
74
+ Also supported is `Resque.enqueue_in` which takes an amount of time in seconds
75
+ in which to queue the job.
76
+
77
+ The delayed queue is stored in redis and is persisted in the same way the
78
+ standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
79
+ from scheduled jobs in that if your scheduler process is down or workers are
80
+ down when a particular job is supposed to be queue, they will simply "catch up"
81
+ once they are started again. Jobs are guaranteed to run (provided they make it
82
+ into the delayed queue) after their given queue_at time has passed.
83
+
84
+ One other thing to note is that insertion into the delayed queue is O(log(n))
85
+ since the jobs are stored in a redis sorted set (zset). I can't imagine this
86
+ being an issue for someone since redis is stupidly fast even at log(n), but full
87
+ disclosure is always best.
88
+
89
+ *Removing Delayed jobs*
90
+
91
+ If you have the need to cancel a delayed job, you can do so thusly:
92
+
93
+ # after you've enqueued a job like:
94
+ Resque.enqueue_at(5.days.from_now, SendFollowUpEmail, :user_id => current_user.id)
95
+ # remove the job with exactly the same parameters:
96
+ Resque.remove_delayed(SendFollowUpEmail, :user_id => current_user.id)
97
+
98
+ ### Schedule jobs per environment
99
+
100
+ Resque-Scheduler allows to create schedule jobs for specific envs. The arg
101
+ `rails_env` (optional) can be used to determine which envs are concerned by the
102
+ job:
103
+
104
+ create_fake_leaderboards:
105
+ cron: "30 6 * * 1"
106
+ class: CreateFakeLeaderboards
107
+ queue: scoring
108
+ args:
109
+ rails_env: demo
110
+ description: "This job will auto-create leaderboards for our online demo"
111
+
112
+ The scheduled job create_fake_leaderboards will be created only if the
113
+ environment variable `RAILS_ENV` is set to demo:
114
+
115
+ $ RAILS_ENV=demo rake resque:scheduler
116
+
117
+ NOTE: If you have added the 2 lines bellow to your Rails Rakefile
118
+ (ie: lib/tasks/resque-scheduler.rake), the rails env is loaded automatically
119
+ and you don't have to specify RAILS_ENV if the var is correctly set in
120
+ environment.rb
121
+
122
+ Alternatively, you can use your resque initializer to avoid loading the entire
123
+ rails stack.
124
+
125
+ $ rake resque:scheduler INITIALIZER_PATH=config/initializers/resque.rb
126
+
127
+
128
+ Multiple envs are allowed, separated by commas:
129
+
130
+ create_fake_leaderboards:
131
+ cron: "30 6 * * 1"
132
+ class: CreateFakeLeaderboards
133
+ queue: scoring
134
+ args:
135
+ rails_env: demo, staging, production
136
+ description: "This job will auto-create leaderboards"
137
+
138
+ NOTE: If you specify the `rails_env` arg without setting RAILS_ENV as an
139
+ environment variable, the job won't be loaded.
140
+
141
+ ### Dynamic Schedules
142
+
143
+ If you need schedules that are defined based on user interactions inside of your application, this can be completed by loading it initially wherever you configure Resque and defining `Resque.reload_schedule!`:
144
+
145
+ module Resque
146
+ def self.reload_schedule!
147
+ self.schedule = MyScheduleModel.all.inject({}) {|schedule_hash, record|
148
+ schedule_hash[record.name.to_sym] = record.attributes.select{|key, value| key != 'name' }
149
+ schedule_hash
150
+ }
151
+ end
152
+ end
153
+
154
+ Resque.reload_schedule!
155
+
156
+ To have the scheduler reload the schedule you just send it the `USR2` signal.
157
+
158
+ ### Support for customized Job classes
159
+
160
+ Some Resque extensions like [resque-status](http://github.com/quirkey/resque-status) use custom job classes with a slightly different API signature.
161
+ Resque-scheduler isn't trying to support all existing and future custom job classes, instead it supports a schedule flag so you can extend your custom class
162
+ and make it support scheduled job.
163
+
164
+ Let's pretend we have a JobWithStatus class called FakeLeaderboard
165
+
166
+ class FakeLeaderboard < Resque::JobWithStatus
167
+ def perfom
168
+ # do something and keep track of the status
169
+ end
170
+ end
171
+
172
+ create_fake_leaderboards:
173
+ cron: "30 6 * * 1"
174
+ queue: scoring
175
+ custom_job_class: FakeLeaderboard
176
+ args:
177
+ rails_env: demo
178
+ description: "This job will auto-create leaderboards for our online demo and the status will update as the worker makes progress"
179
+
180
+ If your extension doesn't support scheduled job, you would need to extend the custom job class to support the #scheduled method:
181
+
182
+ module Resque
183
+ class JobWithStatus
184
+ # Wrapper API to forward a Resque::Job creation API call into a JobWithStatus call.
185
+ def self.scheduled(queue, klass, *args)
186
+ create(args)
187
+ end
188
+ end
189
+ end
190
+
191
+
192
+ Resque-web additions
193
+ --------------------
194
+
195
+ Resque-scheduler also adds to tabs to the resque-web UI. One is for viewing
196
+ (and manually queueing) the schedule and one is for viewing pending jobs in
197
+ the delayed queue.
198
+
199
+ The Schedule tab:
200
+
201
+ ![The Schedule Tab](http://img.skitch.com/20100111-km2f5gmtpbq23enpujbruj6mgk.png)
202
+
203
+ The Delayed tab:
204
+
205
+ ![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
206
+
207
+ Get get these to show up you need to pass a file to `resque-web` to tell it to
208
+ include the `resque-scheduler` plugin. You probably already have a file somewhere
209
+ where you configure `resque`. It probably looks something like this:
210
+
211
+ require 'resque' # include resque so we can configure it
212
+ Resque.redis = "redis_server:6379" # tell Resque where redis lives
213
+
214
+ Now, you want to add the following:
215
+
216
+ require 'resque_scheduler' # include the resque_scheduler (this makes the tabs show up)
217
+
218
+ And if you have a schedule you want to set, add this:
219
+
220
+ Resque.schedule = YAML.load_file(File.join(RAILS_ROOT, 'config/resque_schedule.yml')) # load the schedule
221
+
222
+ Now make sure you're passing that file to resque-web like so:
223
+
224
+ resque-web ~/yourapp/config/resque_config.rb
225
+
226
+ That should make the scheduler tabs show up in `resque-web`.
227
+
228
+
229
+
230
+ Installation and the Scheduler process
231
+ --------------------------------------
232
+
233
+ To install:
234
+
235
+ gem install resque-scheduler
236
+
237
+ You'll need to add this to your rakefile:
238
+
239
+ require 'resque_scheduler/tasks'
240
+ task "resque:setup" => :environment
241
+
242
+ The scheduler process is just a rake task which is responsible for both queueing
243
+ items from the schedule and polling the delayed queue for items ready to be
244
+ pushed on to the work queues. For obvious reasons, this process never exits.
245
+
246
+ $ rake resque:scheduler
247
+
248
+ Supported environment variables are `VERBOSE` and `MUTE`. If either is set to
249
+ any nonempty value, they will take effect. `VERBOSE` simply dumps more output
250
+ to stdout. `MUTE` does the opposite and silences all output. `MUTE` supercedes
251
+ `VERBOSE`.
252
+
253
+ NOTE: You DO NOT want to run >1 instance of the scheduler. Doing so will result
254
+ in the same job being queued more than once. You only need one instnace of the
255
+ scheduler running per resque instance (regardless of number of machines).
256
+
257
+
258
+ Plagurism alert
259
+ ---------------
260
+
261
+ This was intended to be an extension to resque and so resulted in a lot of the
262
+ code looking very similar to resque, particularly in resque-web and the views. I
263
+ wanted it to be similar enough that someone familiar with resque could easily
264
+ work on resque-scheduler.
265
+
266
+
267
+ Contributing
268
+ ------------
269
+
270
+ For bugs or suggestions, please just open an issue in github.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ load File.expand_path('tasks/resque_scheduler.rake')
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ task :default => :test
6
+
7
+ desc "Run tests"
8
+ task :test do
9
+ Dir['test/*_test.rb'].each do |f|
10
+ require File.expand_path(f)
11
+ end
12
+ end
13
+
14
+
15
+ desc "Build a gem"
16
+ task :gem => [ :test, :gemspec, :build ]
17
+
18
+ begin
19
+ begin
20
+ require 'jeweler'
21
+ rescue LoadError
22
+ puts "Jeweler not available. Install it with: "
23
+ puts "gem install jeweler"
24
+ end
25
+
26
+ require 'resque_scheduler/version'
27
+
28
+ Jeweler::Tasks.new do |gemspec|
29
+ gemspec.name = "brianjlandau-resque-scheduler"
30
+ gemspec.summary = "Light weight job scheduling on top of Resque"
31
+ gemspec.description = %{Light weight job scheduling on top of Resque.
32
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
33
+ Also supports queueing jobs on a fixed, cron-like schedule.}
34
+ gemspec.email = "brianjlandau@gmail.com"
35
+ gemspec.homepage = "http://github.com/brianjlandau/resque-scheduler"
36
+ gemspec.authors = ["Ben VandenBos", "Brian Landau"]
37
+ gemspec.version = ResqueScheduler::Version
38
+
39
+ gemspec.add_dependency "redis", ">= 2.0.1"
40
+ gemspec.add_dependency "resque", ">= 1.8.0"
41
+ gemspec.add_dependency "rufus-scheduler"
42
+ gemspec.add_development_dependency "jeweler"
43
+ gemspec.add_development_dependency "mocha"
44
+ gemspec.add_development_dependency "rack-test"
45
+ end
46
+
47
+ Jeweler::GemcutterTasks.new
48
+ end
@@ -0,0 +1,195 @@
1
+ require 'rufus/scheduler'
2
+ require 'thwait'
3
+
4
+ module Resque
5
+
6
+ class Scheduler
7
+
8
+ extend Resque::Helpers
9
+
10
+ class << self
11
+
12
+ # If true, logs more stuff...
13
+ attr_accessor :verbose
14
+
15
+ # If set, produces no output
16
+ attr_accessor :mute
17
+
18
+ # Schedule all jobs and continually look for delayed jobs (never returns)
19
+ def run(da)
20
+
21
+ # trap signals
22
+ register_signal_handlers
23
+
24
+ daemonize
25
+
26
+ # Load the schedule into rufus
27
+ load_schedule!
28
+
29
+ # Now start the scheduling part of the loop.
30
+ loop do
31
+ handle_delayed_items
32
+ poll_sleep
33
+ end
34
+
35
+ # never gets here.
36
+ end
37
+
38
+ # For all signals, set the shutdown flag and wait for current
39
+ # poll/enqueing to finish (should be almost istant). In the
40
+ # case of sleeping, exit immediately.
41
+ def register_signal_handlers
42
+ trap("TERM") { shutdown }
43
+ trap("INT") { shutdown }
44
+
45
+ begin
46
+ trap('QUIT') { shutdown }
47
+ trap('USR1') { kill_child }
48
+ trap('USR2') { reload_schedule! }
49
+ rescue ArgumentError
50
+ warn "Signals QUIT and USR1 and USR2 not supported."
51
+ end
52
+ end
53
+
54
+ def daemonize
55
+ Process.daemon(true)
56
+ if File.directory?('tmp/pids')
57
+ pid_file = File.expand_path('tmp/pids')
58
+ File.open(pid_file, 'w'){ |f| f.write(Process.pid) }
59
+ at_exit { File.delete(pid_file) if File.exist?(pid_file) }
60
+ elsif File.directory?('/usr/local/var/run')
61
+ pid_file = '/usr/local/var/run'
62
+ File.open(pid_file, 'w'){ |f| f.write(Process.pid) }
63
+ at_exit { File.delete(pid_file) if File.exist?(pid_file) }
64
+ end
65
+ end
66
+
67
+ # Pulls the schedule from Resque.schedule and loads it into the
68
+ # rufus scheduler instance
69
+ def load_schedule!
70
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
71
+
72
+ Resque.schedule.each do |name, config|
73
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
74
+ # required for the jobs to be scheduled. If rails_env is missing, the
75
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
76
+ # to.
77
+ if config['rails_env'].nil? || rails_env_matches?(config)
78
+ log! "Scheduling #{name} "
79
+ if !config['cron'].nil? && config['cron'].length > 0
80
+ rufus_scheduler.cron config['cron'] do
81
+ log! "queuing #{config['class']} (#{name})"
82
+ enqueue_from_config(config)
83
+ end
84
+ else
85
+ log! "no cron found for #{config['class']} (#{name}) - skipping"
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Returns true if the given schedule config hash matches the current
92
+ # ENV['RAILS_ENV']
93
+ def rails_env_matches?(config)
94
+ config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
95
+ end
96
+
97
+ # Handles queueing delayed items
98
+ def handle_delayed_items
99
+ item = nil
100
+ begin
101
+ if timestamp = Resque.next_delayed_timestamp
102
+ enqueue_delayed_items_for_timestamp(timestamp)
103
+ end
104
+ # continue processing until there are no more ready timestamps
105
+ end while !timestamp.nil?
106
+ end
107
+
108
+ # Enqueues all delayed jobs for a timestamp
109
+ def enqueue_delayed_items_for_timestamp(timestamp)
110
+ item = nil
111
+ begin
112
+ handle_shutdown do
113
+ if item = Resque.next_item_for_timestamp(timestamp)
114
+ log "queuing #{item['class']} [delayed]"
115
+ queue = item['queue'] || Resque.queue_from_class(constantize(item['class']))
116
+ # Support custom job classes like job with status
117
+ if (job_klass = item['custom_job_class']) && (job_klass != 'Resque::Job')
118
+ # custom job classes not supporting the same API calls must implement the #schedule method
119
+ constantize(job_klass).scheduled(queue, item['class'], *item['args'])
120
+ else
121
+ Resque::Job.create(queue, item['class'], *item['args'])
122
+ end
123
+ end
124
+ end
125
+ # continue processing until there are no more ready items in this timestamp
126
+ end while !item.nil?
127
+ end
128
+
129
+ def handle_shutdown
130
+ exit if @shutdown
131
+ yield
132
+ exit if @shutdown
133
+ end
134
+
135
+ # Enqueues a job based on a config hash
136
+ def enqueue_from_config(config)
137
+ args = config['args'] || config[:args]
138
+ klass_name = config['class'] || config[:class]
139
+ params = args.nil? ? [] : Array(args)
140
+ queue = config['queue'] || config[:queue] || Resque.queue_from_class(constantize(klass_name))
141
+ # Support custom job classes like job with status
142
+ if (job_klass = config['custom_job_class']) && (job_klass != 'Resque::Job')
143
+ # custom job classes not supporting the same API calls must implement the #schedule method
144
+ constantize(job_klass).scheduled(queue, klass_name, *params)
145
+ else
146
+ Resque::Job.create(queue, klass_name, *params)
147
+ end
148
+ end
149
+
150
+ def rufus_scheduler
151
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
152
+ end
153
+
154
+ # Stops old rufus scheduler and creates a new one. Returns the new
155
+ # rufus scheduler
156
+ def clear_schedule!
157
+ rufus_scheduler.stop
158
+ @rufus_scheduler = nil
159
+ rufus_scheduler
160
+ end
161
+
162
+ def reload_schedule!
163
+ clear_schedule!
164
+ Resque.reload_schedule!
165
+ load_schedule!
166
+ end
167
+
168
+ # Sleeps and returns true
169
+ def poll_sleep
170
+ @sleeping = true
171
+ handle_shutdown { sleep 5 }
172
+ @sleeping = false
173
+ true
174
+ end
175
+
176
+ # Sets the shutdown flag, exits if sleeping
177
+ def shutdown
178
+ @shutdown = true
179
+ exit if @sleeping
180
+ end
181
+
182
+ def log!(msg)
183
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
184
+ end
185
+
186
+ def log(msg)
187
+ # add "verbose" logic later
188
+ log!(msg) if verbose
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+ end