resque_cmdline 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b15e6c5c4c89f45182b04e3e9be21ebe59e5428a
4
+ data.tar.gz: e7be2715b86572b97996a84064a5996a3e3923a8
5
+ SHA512:
6
+ metadata.gz: 9edc7668525efd1574eb7bcb3ee4109f3a8842d39c93f6a911d7a716011542e43d7e5eda158458d69f63971d4eab230d6bc739fc019b0a68fd9571d27fbd6d65
7
+ data.tar.gz: 5b1efafa0d7583536eab400feedb7492c71ebec339cd04d69fb0bb35ad7b08cfe369f40712db62c202712e51c6aeaeef875353849ad5979a70191310e5fada00
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque_cmdline.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2012 Ric Lister
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ this software and associated documentation files (the "Software"), to deal in
6
+ the Software without restriction, including without limitation the rights to
7
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8
+ of the Software, and to permit persons to whom the Software is furnished to do
9
+ so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,46 @@
1
+ # resque_cmdline
2
+
3
+ This is a simple command-line client for showing info about queues and
4
+ workers from
5
+ [resque](https://github.com/resque/resque) or
6
+ [sidekiq](https://github.com/mperham/sidekiq).
7
+
8
+ resque_cmdline does not use resque or sidekiq gems, but extracts info
9
+ directly from redis. This should be an order of magnitude faster when
10
+ querying a redis server over a slow (vpn over internet) connection.
11
+
12
+ ## Usage
13
+
14
+ rq [options] command [-e shell_command]
15
+
16
+ where command is one of: queues workers failed running pending stats. Commands
17
+ may be be shortened to their shortest unique beginning.
18
+
19
+ See option list using `-h`.
20
+
21
+ ## Example
22
+
23
+ Show ps listing for all running jobs older than 1 hour:
24
+
25
+ rq running -o 1h -e ssh {host} ps -lfp {pid}
26
+
27
+ ## Config
28
+
29
+ Configure your environments and redis servers in `~/.rq.yml`. For example:
30
+
31
+ ```yaml
32
+ environments:
33
+ production: redis-01:6379
34
+ staging: redis-stg-01:6379
35
+ development: localhost:6379
36
+
37
+ environment: production
38
+ ```
39
+
40
+ ## License
41
+
42
+ See included file LICENSE.
43
+
44
+ ## Copyright
45
+
46
+ Copyright (c) 2012 Richard Lister.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rainbow"
4
+
5
+ ## begin version management
6
+ def valid? version
7
+ pattern = /^\d+\.\d+\.\d+(\-(dev|beta|rc\d+))?$/
8
+ raise "Tried to set invalid version: #{version}".color(:red) unless version =~ pattern
9
+ end
10
+
11
+ def correct_version version
12
+ ver, flag = version.split '-'
13
+ v = ver.split '.'
14
+ (0..2).each do |n|
15
+ v[n] = v[n].to_i
16
+ end
17
+ [v.join('.'), flag].compact.join '-'
18
+ end
19
+
20
+ def read_version
21
+ begin
22
+ File.read 'VERSION'
23
+ rescue
24
+ raise "VERSION file not found or unreadable.".color(:red)
25
+ end
26
+ end
27
+
28
+ def write_version version
29
+ valid? version
30
+ begin
31
+ File.open 'VERSION', 'w' do |file|
32
+ file.write correct_version(version)
33
+ end
34
+ rescue
35
+ raise "VERSION file not found or unwritable.".color(:red)
36
+ end
37
+ end
38
+
39
+ def reset current, which
40
+ version, flag = current.split '-'
41
+ v = version.split '.'
42
+ which.each do |part|
43
+ v[part] = 0
44
+ end
45
+ [v.join('.'), flag].compact.join '-'
46
+ end
47
+
48
+ def increment current, which
49
+ version, flag = current.split '-'
50
+ v = version.split '.'
51
+ v[which] = v[which].to_i + 1
52
+ [v.join('.'), flag].compact.join '-'
53
+ end
54
+
55
+ desc "Prints the current application version"
56
+ version = read_version
57
+ task :version do
58
+ puts <<HELP
59
+ Available commands are:
60
+ -----------------------
61
+ rake version:write[version] # set version explicitly
62
+ rake version:patch # increment the patch x.x.x+1
63
+ rake version:minor # increment minor and reset patch x.x+1.0
64
+ rake version:major # increment major and reset others x+1.0.0
65
+
66
+ HELP
67
+ puts "Current version is: #{version.color(:green)}"
68
+ puts "NOTE: version should always be in the format of x.x.x".color(:red)
69
+ end
70
+
71
+ namespace :version do
72
+
73
+ desc "Write version explicitly by specifying version number as a parameter"
74
+ task :write, [:version] do |task, args|
75
+ write_version args[:version].strip
76
+ puts "Version explicitly written: #{read_version.color(:green)}"
77
+ end
78
+
79
+ desc "Increments the patch version"
80
+ task :patch do
81
+ new_version = increment read_version, 2
82
+ write_version new_version
83
+ puts "Application patched: #{new_version.color(:green)}"
84
+ end
85
+
86
+ desc "Increments the minor version and resets the patch"
87
+ task :minor do
88
+ incremented = increment read_version, 1
89
+ new_version = reset incremented, [2]
90
+ write_version new_version
91
+ puts "New version released: #{new_version.color(:green)}"
92
+ end
93
+
94
+ desc "Increments the major version and resets both minor and patch"
95
+ task :major do
96
+ incremented = increment read_version, 0
97
+ new_version = reset incremented, [1, 2]
98
+ write_version new_version
99
+ puts "Major application version change: #{new_version.color(:green)}. Congratulations!"
100
+ end
101
+
102
+ end
103
+ ## end version management
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/rq ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ## SYNOPSIS
4
+ ##
5
+ ## Fast cmdline client for extracting resque/sidekiq worker info from redis,
6
+ ## displaying info with optional filters for age and class, and optionally
7
+ ## running shell commands on each worker (with hostname and port passed).
8
+ ## Call as 'rq' for resque, or 'sq' for sidekiq (or use --prefix).
9
+ ##
10
+ ## USAGE
11
+ ##
12
+ ## rq [options] command [-e shell_command]
13
+ ##
14
+ ## where command is one of: queues workers failed running pending stats. Commands
15
+ ## may be be shortened to their shortest unique beginning.
16
+ ##
17
+ ## See option list using -h.
18
+ ##
19
+ ## EXAMPLE
20
+ ##
21
+ ## Show ps listing for all running jobs older than 1 hour:
22
+ ##
23
+ ## rq running -o 1h -e ssh {host} ps -lfp {pid}
24
+ ##
25
+ ## CONFIG
26
+ ##
27
+ ## Configure your environments and redis servers in ~/.rq.yml. For example:
28
+ ##
29
+ ## environments:
30
+ ## production: redis-01:6379
31
+ ## staging: redis-stg-01:6379
32
+ ## development: localhost:6379
33
+ ## environment: production
34
+ ##
35
+ ## LICENSE
36
+ ##
37
+ ## See included file LICENSE.
38
+ ##
39
+ ## COPYRIGHT
40
+ ##
41
+ ## Copyright (c) 2012 Richard Lister.
42
+
43
+ require "redis"
44
+ require "json"
45
+ require "optparse"
46
+ require 'date'
47
+ require 'yaml'
48
+
49
+ class Object
50
+ def try(method)
51
+ send method if respond_to? method
52
+ end
53
+ end
54
+
55
+ ## convert array of arrays to array of strings with columns aligned
56
+ class Array
57
+ def values
58
+ self
59
+ end
60
+
61
+ def tabulate
62
+ widths = self.map do |row|
63
+ row.values.map { |value| value.to_s.length }
64
+ end.transpose.map(&:max) # => array of columns widths
65
+
66
+ self.map do |row|
67
+ row.values.each_with_index.map do |cell, index|
68
+ "%-#{widths[index]}s" % cell
69
+ end.join(' ')
70
+ end # => array of formatted table rows as strings
71
+ end
72
+ end
73
+
74
+ ## return new hash with all keys recursively turned into symbols
75
+ class Hash
76
+ def symbolize_keys
77
+ Hash[self.map do |key, value|
78
+ [ key.to_sym, value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value ]
79
+ end]
80
+ end
81
+ end
82
+
83
+ ## human-readable time in past relative to present
84
+ class String
85
+ def seconds_ago
86
+ DateTime.parse(self).to_time.seconds_ago
87
+ end
88
+
89
+ def to_seconds
90
+ multiplier = { '' => 1, 's' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800 }
91
+ /^(\d+)\s*([smhdw]?)/.match(self) or raise ArgumentError, "Illegal time period #{self}"
92
+ Integer($1) * multiplier[$2]
93
+ end
94
+ end
95
+
96
+ ## assume sidekiq timestamps given as int are epochs
97
+ class Fixnum
98
+ def seconds_ago
99
+ Time.at(self).to_time.seconds_ago
100
+ end
101
+ end
102
+
103
+ class Time
104
+ def seconds_ago
105
+ Time.now.utc.to_i - self.utc.to_i
106
+ end
107
+ end
108
+
109
+ ## convert time in secs to human-readable units
110
+ class Fixnum
111
+ def to_dhms
112
+ m, s = self.divmod(60)
113
+ h, m = m.divmod(60)
114
+ d, h = h.divmod(24)
115
+ {:d => d, :h => h, :m => m, :s => s}.map do |k,v|
116
+ v>0 ? "#{v}#{k}" : nil
117
+ end.compact.join(" ")
118
+ end
119
+ end
120
+
121
+ class ResqueCmdline
122
+ attr_reader :redis, :options, :prefix
123
+
124
+ def initialize(options = nil)
125
+ @options = options
126
+ host, port = options[:redis].split(':')
127
+ @redis = Redis.new(:host => host, :port => port || 6379)
128
+ @prefix = options[:prefix]
129
+ end
130
+
131
+ def queues
132
+ redis.smembers("#{prefix}queues").sort.map { |q| [q] }
133
+ end
134
+
135
+ def workers
136
+ redis.smembers("#{prefix}workers").sort.map { |w| [w] }
137
+ end
138
+
139
+ def failed
140
+ failed = redis.lrange("#{prefix}failed", 0, -1).map do |f|
141
+ job = JSON(f)
142
+ {
143
+ :worker => job['worker'].match(/^([\w\.-]+:\d+):/).captures.join,
144
+ :queue => job['queue'],
145
+ :class => job['payload']['class'],
146
+ :exception => job['exception'],
147
+ :date => job['failed_at'].seconds_ago,
148
+ :retried => job['retried_at'].try(:seconds_ago), # can be nil
149
+ }
150
+ end
151
+
152
+ if options[:older]
153
+ failed.select! { |job| job[:date] >= options[:older] }
154
+ end
155
+
156
+ if options[:retried]
157
+ failed.select! { |job| job[:retried] and job[:retried] >= options[:retried] }
158
+ end
159
+
160
+ failed.sort{ |a,b| b[:date] <=> a[:date] }.each do |job|
161
+ job[:date] = job[:date].to_dhms
162
+ job[:retried] = job[:retried].try(:to_dhms) # can be nil
163
+ end
164
+ end
165
+
166
+ def unregister(hostport)
167
+ redis.smembers("#{prefix}workers").grep(/^#{hostport}/).each do |worker|
168
+ puts "unregistering: #{worker}"
169
+ redis.srem("#{prefix}workers", worker)
170
+ redis.del("#{prefix}worker:#{worker}")
171
+ redis.del("#{prefix}worker:#{worker}:started")
172
+ redis.del("#{prefix}stat:processed:#{worker}")
173
+ redis.del("#{prefix}stat:failed:#{worker}")
174
+ end
175
+ end
176
+
177
+ def running
178
+ keys = redis.smembers("#{prefix}workers").map { |worker| "#{prefix}worker:#{worker}" }
179
+ return keys if keys.empty?
180
+
181
+ running = redis.mapped_mget(*keys).map do |key, value|
182
+ if value.nil? || value.empty?
183
+ nil
184
+ else
185
+ job = JSON(value)
186
+ {
187
+ :worker => key.match(/^#{prefix}worker:([\w\.-]+:[\d\-]+):/).captures.join,
188
+ :queue => job['queue'],
189
+ :class => job['payload']['class'],
190
+ :date => job['run_at'].seconds_ago,
191
+ }
192
+ end
193
+ end.compact
194
+
195
+ if options[:older]
196
+ running.select! { |job| job[:date] >= options[:older] }
197
+ end
198
+
199
+ running.sort { |a,b| b[:date] <=> a[:date] }.each do |job|
200
+ job[:date] = job[:date].to_dhms
201
+ end
202
+ end
203
+
204
+ def pending
205
+ redis.smembers("#{prefix}queues").map do |queue|
206
+ {
207
+ :name => queue,
208
+ :count => redis.llen("#{prefix}queue:#{queue}")
209
+ }
210
+ end.sort { |a,b| b[:count] <=> a[:count] }
211
+ end
212
+
213
+ def stats
214
+ [
215
+ [ "total processed", redis.get("#{prefix}stat:processed").to_s ],
216
+ [ "total failed", redis.get("#{prefix}stat:failed").to_s ],
217
+ [ "queues", queues.size.to_s ],
218
+ [ "workers", redis.scard("#{prefix}workers").to_s ],
219
+ [ "pending", queues.inject(0) { |sum, q| sum + redis.llen("#{prefix}queue:#{q}") }.to_s ],
220
+ [ "failed", redis.llen("#{prefix}failed").to_s ],
221
+ ]
222
+ end
223
+
224
+ end
225
+
226
+ ## defaults
227
+ options = {
228
+ :environments => {
229
+ :production => 'http://redis:6379',
230
+ :development => 'http://localhost:6379'
231
+ },
232
+ :environment => :production,
233
+ #:prefix => 'resque:',
234
+ :prefix => File.basename($0) == 'sq' ? '' : 'resque:', #sidekiq vs resque
235
+ }
236
+
237
+ ## merge options from config file
238
+ cfgfile = File.join(ENV['HOME'], '.rq.yml')
239
+ options = options.merge(YAML.load_file(cfgfile).symbolize_keys) if File.exists?(cfgfile)
240
+
241
+ commands = %w[queues workers failed running pending stats]
242
+
243
+ OptionParser.new do |opt|
244
+ opt.banner = "Usage: #{$0} [options] command [-e shellcmd]\n(use -h for help)."
245
+ opt.on('-c', '--class REGEX', 'Filter by class name.') do |c|
246
+ options[:class] = c
247
+ end
248
+ opt.on('-e', '--exec CMD', 'Exec cmd with sub for {host} and {pid}, must be last arg.') do |e|
249
+ options[:exec] = ARGV.shift(ARGV.length).unshift(e).join(' ') # slurp rest of ARGV
250
+ end
251
+ opt.on('-E', '--environment REGEX', 'Environment to use.') do |e|
252
+ options[:environment] = e
253
+ end
254
+ opt.on('-j', '--json', 'Dump output as JSON instead of tabulated output.') do |j|
255
+ options[:json] = j
256
+ end
257
+ opt.on('-o', '--older TIME', 'Filter date e.g. 30s, 2d.') do |o|
258
+ options[:older] = o.to_seconds
259
+ end
260
+ opt.on('-p', '--prefix PREFIX', 'Prefix for keys in redis.') do |p|
261
+ options[:prefix] = p
262
+ end
263
+ opt.on('-q', '--queue REGEX', 'Filter by queue name.') do |q|
264
+ options[:queue] = q
265
+ end
266
+ opt.on('-r', '--retried TIME', 'Filter failed jobs retried before time.') do |r|
267
+ options[:retried] = r.to_seconds
268
+ end
269
+ opt.on('-R', '--redis SERVER' 'Set redis server as host:port') do |r|
270
+ options[:redis] = r
271
+ end
272
+ opt.on('-U', '--unregister', 'Unregister matching workers from redis. Be very careful.') do |u|
273
+ options[:unregister] = u
274
+ end
275
+ opt.on_tail("-h", "--help", "Show this message.") do
276
+ puts opt
277
+ puts " cmd: #{commands.join(' ')}"
278
+ exit
279
+ end
280
+ end.parse!
281
+
282
+
283
+ ## find command that matches arg given
284
+ matching = commands.grep /^#{ARGV.first}/
285
+ abort "commands: #{commands.join(' ')}" unless matching.length == 1
286
+
287
+ ## find first matching environment
288
+ environment, redis = options[:environments].find do |k, v|
289
+ k.match(/^#{options[:environment]}/)
290
+ end
291
+ abort "environments: #{options[:environments].keys.join(' ')}" unless environment && redis
292
+
293
+ ## redis server from environment, unless overridden with option
294
+ options[:redis] ||= redis
295
+
296
+ ## get data from redis
297
+ cmd = ResqueCmdline.new(options)
298
+ output = cmd.send(matching.first)
299
+
300
+ ## grep output by a specific named column
301
+ [:class, :queue].each do |field|
302
+ if options[field]
303
+ output.select! do |row|
304
+ c = row.fetch(field) { nil } and c.match(options[field])
305
+ end
306
+ end
307
+ end
308
+
309
+ ## handle data for exec or output
310
+ if options[:exec]
311
+ output.each do |line|
312
+ worker = line.fetch(:worker) { false } or next
313
+ host, pid = worker.split(':')
314
+ cmd = options[:exec].dup
315
+ cmd.gsub!(/\{host\}/, host).gsub!(/\{pid\}/, pid)
316
+ system cmd
317
+ end
318
+ elsif options[:unregister]
319
+ output.each do |line|
320
+ worker = line.fetch(:worker) { false } or next
321
+ cmd.unregister(worker)
322
+ end
323
+ elsif options[:json]
324
+ puts JSON.dump(output)
325
+ else
326
+ puts output.tabulate
327
+ end
data/bin/sq ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ## SYNOPSIS
4
+ ##
5
+ ## Fast cmdline client for extracting resque/sidekiq worker info from redis,
6
+ ## displaying info with optional filters for age and class, and optionally
7
+ ## running shell commands on each worker (with hostname and port passed).
8
+ ## Call as 'rq' for resque, or 'sq' for sidekiq (or use --prefix).
9
+ ##
10
+ ## USAGE
11
+ ##
12
+ ## rq [options] command [-e shell_command]
13
+ ##
14
+ ## where command is one of: queues workers failed running pending stats. Commands
15
+ ## may be be shortened to their shortest unique beginning.
16
+ ##
17
+ ## See option list using -h.
18
+ ##
19
+ ## EXAMPLE
20
+ ##
21
+ ## Show ps listing for all running jobs older than 1 hour:
22
+ ##
23
+ ## rq running -o 1h -e ssh {host} ps -lfp {pid}
24
+ ##
25
+ ## CONFIG
26
+ ##
27
+ ## Configure your environments and redis servers in ~/.rq.yml. For example:
28
+ ##
29
+ ## environments:
30
+ ## production: redis-01:6379
31
+ ## staging: redis-stg-01:6379
32
+ ## development: localhost:6379
33
+ ## environment: production
34
+ ##
35
+ ## LICENSE
36
+ ##
37
+ ## See included file LICENSE.
38
+ ##
39
+ ## COPYRIGHT
40
+ ##
41
+ ## Copyright (c) 2012 Richard Lister.
42
+
43
+ require "redis"
44
+ require "json"
45
+ require "optparse"
46
+ require 'date'
47
+ require 'yaml'
48
+
49
+ class Object
50
+ def try(method)
51
+ send method if respond_to? method
52
+ end
53
+ end
54
+
55
+ ## convert array of arrays to array of strings with columns aligned
56
+ class Array
57
+ def values
58
+ self
59
+ end
60
+
61
+ def tabulate
62
+ widths = self.map do |row|
63
+ row.values.map { |value| value.to_s.length }
64
+ end.transpose.map(&:max) # => array of columns widths
65
+
66
+ self.map do |row|
67
+ row.values.each_with_index.map do |cell, index|
68
+ "%-#{widths[index]}s" % cell
69
+ end.join(' ')
70
+ end # => array of formatted table rows as strings
71
+ end
72
+ end
73
+
74
+ ## return new hash with all keys recursively turned into symbols
75
+ class Hash
76
+ def symbolize_keys
77
+ Hash[self.map do |key, value|
78
+ [ key.to_sym, value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value ]
79
+ end]
80
+ end
81
+ end
82
+
83
+ ## human-readable time in past relative to present
84
+ class String
85
+ def seconds_ago
86
+ DateTime.parse(self).to_time.seconds_ago
87
+ end
88
+
89
+ def to_seconds
90
+ multiplier = { '' => 1, 's' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800 }
91
+ /^(\d+)\s*([smhdw]?)/.match(self) or raise ArgumentError, "Illegal time period #{self}"
92
+ Integer($1) * multiplier[$2]
93
+ end
94
+ end
95
+
96
+ ## assume sidekiq timestamps given as int are epochs
97
+ class Fixnum
98
+ def seconds_ago
99
+ Time.at(self).to_time.seconds_ago
100
+ end
101
+ end
102
+
103
+ class Time
104
+ def seconds_ago
105
+ Time.now.utc.to_i - self.utc.to_i
106
+ end
107
+ end
108
+
109
+ ## convert time in secs to human-readable units
110
+ class Fixnum
111
+ def to_dhms
112
+ m, s = self.divmod(60)
113
+ h, m = m.divmod(60)
114
+ d, h = h.divmod(24)
115
+ {:d => d, :h => h, :m => m, :s => s}.map do |k,v|
116
+ v>0 ? "#{v}#{k}" : nil
117
+ end.compact.join(" ")
118
+ end
119
+ end
120
+
121
+ class ResqueCmdline
122
+ attr_reader :redis, :options, :prefix
123
+
124
+ def initialize(options = nil)
125
+ @options = options
126
+ host, port = options[:redis].split(':')
127
+ @redis = Redis.new(:host => host, :port => port || 6379)
128
+ @prefix = options[:prefix]
129
+ end
130
+
131
+ def queues
132
+ redis.smembers("#{prefix}queues").sort.map { |q| [q] }
133
+ end
134
+
135
+ def workers
136
+ redis.smembers("#{prefix}workers").sort.map { |w| [w] }
137
+ end
138
+
139
+ def failed
140
+ failed = redis.lrange("#{prefix}failed", 0, -1).map do |f|
141
+ job = JSON(f)
142
+ {
143
+ :worker => job['worker'].match(/^([\w\.-]+:\d+):/).captures.join,
144
+ :queue => job['queue'],
145
+ :class => job['payload']['class'],
146
+ :exception => job['exception'],
147
+ :date => job['failed_at'].seconds_ago,
148
+ :retried => job['retried_at'].try(:seconds_ago), # can be nil
149
+ }
150
+ end
151
+
152
+ if options[:older]
153
+ failed.select! { |job| job[:date] >= options[:older] }
154
+ end
155
+
156
+ if options[:retried]
157
+ failed.select! { |job| job[:retried] and job[:retried] >= options[:retried] }
158
+ end
159
+
160
+ failed.sort{ |a,b| b[:date] <=> a[:date] }.each do |job|
161
+ job[:date] = job[:date].to_dhms
162
+ job[:retried] = job[:retried].try(:to_dhms) # can be nil
163
+ end
164
+ end
165
+
166
+ def unregister(hostport)
167
+ redis.smembers("#{prefix}workers").grep(/^#{hostport}/).each do |worker|
168
+ puts "unregistering: #{worker}"
169
+ redis.srem("#{prefix}workers", worker)
170
+ redis.del("#{prefix}worker:#{worker}")
171
+ redis.del("#{prefix}worker:#{worker}:started")
172
+ redis.del("#{prefix}stat:processed:#{worker}")
173
+ redis.del("#{prefix}stat:failed:#{worker}")
174
+ end
175
+ end
176
+
177
+ def running
178
+ keys = redis.smembers("#{prefix}workers").map { |worker| "#{prefix}worker:#{worker}" }
179
+ return keys if keys.empty?
180
+
181
+ running = redis.mapped_mget(*keys).map do |key, value|
182
+ if value.nil? || value.empty?
183
+ nil
184
+ else
185
+ job = JSON(value)
186
+ {
187
+ :worker => key.match(/^#{prefix}worker:([\w\.-]+:[\d\-]+):/).captures.join,
188
+ :queue => job['queue'],
189
+ :class => job['payload']['class'],
190
+ :date => job['run_at'].seconds_ago,
191
+ }
192
+ end
193
+ end.compact
194
+
195
+ if options[:older]
196
+ running.select! { |job| job[:date] >= options[:older] }
197
+ end
198
+
199
+ running.sort { |a,b| b[:date] <=> a[:date] }.each do |job|
200
+ job[:date] = job[:date].to_dhms
201
+ end
202
+ end
203
+
204
+ def pending
205
+ redis.smembers("#{prefix}queues").map do |queue|
206
+ {
207
+ :name => queue,
208
+ :count => redis.llen("#{prefix}queue:#{queue}")
209
+ }
210
+ end.sort { |a,b| b[:count] <=> a[:count] }
211
+ end
212
+
213
+ def stats
214
+ [
215
+ [ "total processed", redis.get("#{prefix}stat:processed").to_s ],
216
+ [ "total failed", redis.get("#{prefix}stat:failed").to_s ],
217
+ [ "queues", queues.size.to_s ],
218
+ [ "workers", redis.scard("#{prefix}workers").to_s ],
219
+ [ "pending", queues.inject(0) { |sum, q| sum + redis.llen("#{prefix}queue:#{q}") }.to_s ],
220
+ [ "failed", redis.llen("#{prefix}failed").to_s ],
221
+ ]
222
+ end
223
+
224
+ end
225
+
226
+ ## defaults
227
+ options = {
228
+ :environments => {
229
+ :production => 'http://redis:6379',
230
+ :development => 'http://localhost:6379'
231
+ },
232
+ :environment => :production,
233
+ #:prefix => 'resque:',
234
+ :prefix => File.basename($0) == 'sq' ? '' : 'resque:', #sidekiq vs resque
235
+ }
236
+
237
+ ## merge options from config file
238
+ cfgfile = File.join(ENV['HOME'], '.rq.yml')
239
+ options = options.merge(YAML.load_file(cfgfile).symbolize_keys) if File.exists?(cfgfile)
240
+
241
+ commands = %w[queues workers failed running pending stats]
242
+
243
+ OptionParser.new do |opt|
244
+ opt.banner = "Usage: #{$0} [options] command [-e shellcmd]\n(use -h for help)."
245
+ opt.on('-c', '--class REGEX', 'Filter by class name.') do |c|
246
+ options[:class] = c
247
+ end
248
+ opt.on('-e', '--exec CMD', 'Exec cmd with sub for {host} and {pid}, must be last arg.') do |e|
249
+ options[:exec] = ARGV.shift(ARGV.length).unshift(e).join(' ') # slurp rest of ARGV
250
+ end
251
+ opt.on('-E', '--environment REGEX', 'Environment to use.') do |e|
252
+ options[:environment] = e
253
+ end
254
+ opt.on('-j', '--json', 'Dump output as JSON instead of tabulated output.') do |j|
255
+ options[:json] = j
256
+ end
257
+ opt.on('-o', '--older TIME', 'Filter date e.g. 30s, 2d.') do |o|
258
+ options[:older] = o.to_seconds
259
+ end
260
+ opt.on('-p', '--prefix PREFIX', 'Prefix for keys in redis.') do |p|
261
+ options[:prefix] = p
262
+ end
263
+ opt.on('-q', '--queue REGEX', 'Filter by queue name.') do |q|
264
+ options[:queue] = q
265
+ end
266
+ opt.on('-r', '--retried TIME', 'Filter failed jobs retried before time.') do |r|
267
+ options[:retried] = r.to_seconds
268
+ end
269
+ opt.on('-R', '--redis SERVER' 'Set redis server as host:port') do |r|
270
+ options[:redis] = r
271
+ end
272
+ opt.on('-U', '--unregister', 'Unregister matching workers from redis. Be very careful.') do |u|
273
+ options[:unregister] = u
274
+ end
275
+ opt.on_tail("-h", "--help", "Show this message.") do
276
+ puts opt
277
+ puts " cmd: #{commands.join(' ')}"
278
+ exit
279
+ end
280
+ end.parse!
281
+
282
+
283
+ ## find command that matches arg given
284
+ matching = commands.grep /^#{ARGV.first}/
285
+ abort "commands: #{commands.join(' ')}" unless matching.length == 1
286
+
287
+ ## find first matching environment
288
+ environment, redis = options[:environments].find do |k, v|
289
+ k.match(/^#{options[:environment]}/)
290
+ end
291
+ abort "environments: #{options[:environments].keys.join(' ')}" unless environment && redis
292
+
293
+ ## redis server from environment, unless overridden with option
294
+ options[:redis] ||= redis
295
+
296
+ ## get data from redis
297
+ cmd = ResqueCmdline.new(options)
298
+ output = cmd.send(matching.first)
299
+
300
+ ## grep output by a specific named column
301
+ [:class, :queue].each do |field|
302
+ if options[field]
303
+ output.select! do |row|
304
+ c = row.fetch(field) { nil } and c.match(options[field])
305
+ end
306
+ end
307
+ end
308
+
309
+ ## handle data for exec or output
310
+ if options[:exec]
311
+ output.each do |line|
312
+ worker = line.fetch(:worker) { false } or next
313
+ host, pid = worker.split(':')
314
+ cmd = options[:exec].dup
315
+ cmd.gsub!(/\{host\}/, host).gsub!(/\{pid\}/, pid)
316
+ system cmd
317
+ end
318
+ elsif options[:unregister]
319
+ output.each do |line|
320
+ worker = line.fetch(:worker) { false } or next
321
+ cmd.unregister(worker)
322
+ end
323
+ elsif options[:json]
324
+ puts JSON.dump(output)
325
+ else
326
+ puts output.tabulate
327
+ end
@@ -0,0 +1,5 @@
1
+ require "resque_cmdline/version"
2
+
3
+ module ResqueCmdline
4
+ # the code is all in ../bin
5
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueCmdline
2
+ VERSION = File.read(File.join(File.dirname(__FILE__), "..", "..", "VERSION"))
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque_cmdline/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "resque_cmdline"
8
+ spec.version = ResqueCmdline::VERSION
9
+ spec.authors = ["Ric Lister"]
10
+ spec.email = ["rlister@gmail.com"]
11
+ spec.description = %q{resque_cmdline: simple command-line client for resque workers}
12
+ spec.summary = %q{Ruby command-line client for resque and sidekiq workers}
13
+ spec.homepage = "https://github.com/rlister/resque_cmdline"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ # dependencies
22
+ spec.add_dependency('json' , '>= 1.7.5')
23
+ spec.add_dependency('redis' , '>= 3.0.2')
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rainbow"
28
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque_cmdline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ric Lister
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.7.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rainbow
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: 'resque_cmdline: simple command-line client for resque workers'
84
+ email:
85
+ - rlister@gmail.com
86
+ executables:
87
+ - rq
88
+ - sq
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - .gitignore
93
+ - Gemfile
94
+ - LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - VERSION
98
+ - bin/rq
99
+ - bin/sq
100
+ - lib/resque_cmdline.rb
101
+ - lib/resque_cmdline/version.rb
102
+ - resque_cmdline.gemspec
103
+ homepage: https://github.com/rlister/resque_cmdline
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.0.0
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Ruby command-line client for resque and sidekiq workers
127
+ test_files: []