qless 0.9.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.
- data/Gemfile +8 -0
- data/HISTORY.md +168 -0
- data/README.md +571 -0
- data/Rakefile +28 -0
- data/bin/qless-campfire +106 -0
- data/bin/qless-growl +99 -0
- data/bin/qless-web +23 -0
- data/lib/qless.rb +185 -0
- data/lib/qless/config.rb +31 -0
- data/lib/qless/job.rb +259 -0
- data/lib/qless/job_reservers/ordered.rb +23 -0
- data/lib/qless/job_reservers/round_robin.rb +34 -0
- data/lib/qless/lua.rb +25 -0
- data/lib/qless/qless-core/cancel.lua +71 -0
- data/lib/qless/qless-core/complete.lua +218 -0
- data/lib/qless/qless-core/config.lua +44 -0
- data/lib/qless/qless-core/depends.lua +65 -0
- data/lib/qless/qless-core/fail.lua +107 -0
- data/lib/qless/qless-core/failed.lua +83 -0
- data/lib/qless/qless-core/get.lua +37 -0
- data/lib/qless/qless-core/heartbeat.lua +50 -0
- data/lib/qless/qless-core/jobs.lua +41 -0
- data/lib/qless/qless-core/peek.lua +155 -0
- data/lib/qless/qless-core/pop.lua +278 -0
- data/lib/qless/qless-core/priority.lua +32 -0
- data/lib/qless/qless-core/put.lua +156 -0
- data/lib/qless/qless-core/queues.lua +58 -0
- data/lib/qless/qless-core/recur.lua +181 -0
- data/lib/qless/qless-core/retry.lua +73 -0
- data/lib/qless/qless-core/ruby/lib/qless-core.rb +1 -0
- data/lib/qless/qless-core/ruby/lib/qless/core.rb +13 -0
- data/lib/qless/qless-core/ruby/lib/qless/core/version.rb +5 -0
- data/lib/qless/qless-core/ruby/spec/qless_core_spec.rb +13 -0
- data/lib/qless/qless-core/stats.lua +92 -0
- data/lib/qless/qless-core/tag.lua +100 -0
- data/lib/qless/qless-core/track.lua +79 -0
- data/lib/qless/qless-core/workers.lua +69 -0
- data/lib/qless/queue.rb +141 -0
- data/lib/qless/server.rb +411 -0
- data/lib/qless/tasks.rb +10 -0
- data/lib/qless/version.rb +3 -0
- data/lib/qless/worker.rb +195 -0
- metadata +239 -0
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
9
|
+
t.rspec_opts = %w[--profile --format progress]
|
10
|
+
t.ruby_opts = "-Ispec -rsimplecov_setup"
|
11
|
+
end
|
12
|
+
|
13
|
+
# TODO: bump this up as test coverage increases. It was 90.29 when I last updated it on 2012-05-21.
|
14
|
+
min_coverage_threshold = 90.0
|
15
|
+
desc "Checks the spec coverage and fails if it is less than #{min_coverage_threshold}%"
|
16
|
+
task :check_coverage do
|
17
|
+
percent = File.read("./coverage/coverage_percent.txt").to_f
|
18
|
+
if percent < min_coverage_threshold
|
19
|
+
raise "Spec coverage was not high enough: #{percent.round(2)}%"
|
20
|
+
else
|
21
|
+
puts "Nice job! Spec coverage is still at least #{min_coverage_threshold}%"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
task default: [:spec, :check_coverage]
|
26
|
+
|
27
|
+
|
28
|
+
require 'qless/tasks'
|
data/bin/qless-campfire
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'qless'
|
6
|
+
require 'tinder'
|
7
|
+
require 'micro-optparse'
|
8
|
+
|
9
|
+
@options = Parser.new do |p|
|
10
|
+
p.banner = 'This agent lets you get campfire notifications for the progress of tracked jobs'
|
11
|
+
p.option :subdomain, 'campfire subdomain' , :default => '', :value_satisfies => lambda { |subdomain| subdomain.is_a?(String) }
|
12
|
+
p.option :token , 'campfire token for bot', :default => '', :value_satisfies => lambda { |subdomain| subdomain.is_a?(String) }
|
13
|
+
p.option :room , 'campfire room to talk in (defaults to first room)', :default => ''
|
14
|
+
p.option :host , 'host:port for your qless redis instance', :default => 'localhost:6379'
|
15
|
+
p.option :web , 'host:port for your qless web ui', :default => 'localhost:5678'
|
16
|
+
end.process!
|
17
|
+
|
18
|
+
# Get a campfire object, and room
|
19
|
+
puts 'Connecting to campfire...'
|
20
|
+
@campfire = Tinder::Campfire.new @options[:subdomain], :token => @options[:token]
|
21
|
+
if not @options[:room]
|
22
|
+
@room = @campfire.rooms.first
|
23
|
+
else
|
24
|
+
@room = @campfire.find_room_by_name(@options[:room])
|
25
|
+
end
|
26
|
+
|
27
|
+
# And now qless stuff
|
28
|
+
puts 'Connecting to qless...'
|
29
|
+
@client = Qless::Client.new(:host => @options[:host])
|
30
|
+
|
31
|
+
def speak(message)
|
32
|
+
@room.speak message
|
33
|
+
puts "Said '#{message}'"
|
34
|
+
end
|
35
|
+
|
36
|
+
def paste(message)
|
37
|
+
@room.paste message
|
38
|
+
puts "Pasted '#{message}'"
|
39
|
+
end
|
40
|
+
|
41
|
+
def event(jid, &block)
|
42
|
+
job = @client.jobs[jid]
|
43
|
+
if job.nil?
|
44
|
+
speak("#{jid[0...8]} #{yield job} | http://#{@options[:host]}/jobs/#{jid}]")
|
45
|
+
else
|
46
|
+
speak("#{jid[0...8]} #{yield job} [#{job.tags.join(', ')}] | http://#{@options[:host]}/jobs/#{jid}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@client.events.listen do |on|
|
51
|
+
on.canceled { |jid| event(jid) { |job| 'canceled' } }
|
52
|
+
on.stalled { |jid| event(jid) { |job| 'stalled' } }
|
53
|
+
on.track { |jid| event(jid) { |job| 'is being tracked' } }
|
54
|
+
on.untrack { |jid| event(jid) { |job| 'not longer tracked' } }
|
55
|
+
|
56
|
+
on.completed do |jid|
|
57
|
+
event(jid) do |job|
|
58
|
+
if job
|
59
|
+
if job.queue_name.empty?
|
60
|
+
'completed'
|
61
|
+
else
|
62
|
+
"advanced to #{job.queue_name}"
|
63
|
+
end
|
64
|
+
else
|
65
|
+
'completed'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
on.popped do |jid|
|
71
|
+
event(jid) do |job|
|
72
|
+
if job
|
73
|
+
"popped by #{job.worker_name}"
|
74
|
+
else
|
75
|
+
'popped'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
on.put do |jid|
|
81
|
+
event(jid) do |job|
|
82
|
+
if job
|
83
|
+
"moved to #{job.queue_name}"
|
84
|
+
else
|
85
|
+
'moved'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
on.failed do |jid|
|
91
|
+
job = @client.jobs[jid]
|
92
|
+
if job
|
93
|
+
speak("#{jid} failed by #{job.failure['worker']} | http://#{@options[:web]}/jobs/#{jid}")
|
94
|
+
paste job.failure['message']
|
95
|
+
else
|
96
|
+
speak("#{jid} failed")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
puts 'Listening...'
|
101
|
+
|
102
|
+
Signal.trap("INT") do
|
103
|
+
puts 'Exiting'
|
104
|
+
Process.exit(0)
|
105
|
+
end
|
106
|
+
end
|
data/bin/qless-growl
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'qless'
|
6
|
+
require 'ruby-growl'
|
7
|
+
require 'micro-optparse'
|
8
|
+
|
9
|
+
@options = Parser.new do |p|
|
10
|
+
p.banner = 'This agent lets you get campfire notifications for the progress of tracked jobs'
|
11
|
+
p.option :growl , 'host for the growl daemon', :default => 'localhost'
|
12
|
+
p.option :app , 'application name for notifications', :default => 'qless'
|
13
|
+
p.option :host , 'host:port for your qless redis instance', :default => 'localhost:6379'
|
14
|
+
p.option :web , 'host:port for your qless web ui', :default => 'localhost:5678'
|
15
|
+
end.process!
|
16
|
+
|
17
|
+
# Connect to growl
|
18
|
+
puts 'Connecting to growl...'
|
19
|
+
@growl = Growl::GNTP.new @options[:growl], @options[:app]
|
20
|
+
|
21
|
+
# Qless client
|
22
|
+
puts 'Connecting to qless...'
|
23
|
+
@client = Qless::Client.new(:host => @options[:host])
|
24
|
+
|
25
|
+
%w{canceled completed failed popped stalled put track untrack}.each do |t|
|
26
|
+
@growl.add_notification(t)
|
27
|
+
end
|
28
|
+
@growl.register
|
29
|
+
|
30
|
+
def notify(jid, event, &block)
|
31
|
+
job = @client.jobs[jid]
|
32
|
+
if job.nil?
|
33
|
+
message = yield job
|
34
|
+
puts "#{jid} -> Notifying #{message}"
|
35
|
+
@growl.notify(event, "#{jid[0...8]}", "#{message}", 0, false, jid, "http://#{@options[:web]}/jobs/#{jid}")
|
36
|
+
else
|
37
|
+
message = yield job
|
38
|
+
puts "#{jid} -> Notifying #{message}"
|
39
|
+
@growl.notify(event, "#{jid[0...8]} [#{job.tags.join(', ')}]", "#{message}", 0, false, jid, "http://#{@options[:web]}/jobs/#{jid}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@client.events.listen do |on|
|
44
|
+
on.canceled { |jid| notify(jid, 'canceled') { |job| 'canceled' } }
|
45
|
+
on.stalled { |jid| notify(jid, 'canceled') { |job| 'stalled' } }
|
46
|
+
on.track { |jid| notify(jid, 'canceled') { |job| 'is being tracked' } }
|
47
|
+
on.untrack { |jid| notify(jid, 'canceled') { |job| 'no longer tracked' } }
|
48
|
+
|
49
|
+
on.completed do |jid|
|
50
|
+
notify(jid, 'completed') do |job|
|
51
|
+
if job
|
52
|
+
if job.queue_name.empty?
|
53
|
+
"completed"
|
54
|
+
else
|
55
|
+
"advanced to #{job.queue_name}"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
"completed"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
on.failed do |jid|
|
64
|
+
notify(jid, 'failed') do |job|
|
65
|
+
if job
|
66
|
+
"failed by #{job.failure['worker']}"
|
67
|
+
else
|
68
|
+
"failed"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
on.popped do |jid|
|
74
|
+
notify(jid, 'popped') do |job|
|
75
|
+
if job
|
76
|
+
"popped by #{job.worker_name}"
|
77
|
+
else
|
78
|
+
"popped"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
on.put do |jid|
|
84
|
+
notify(jid, 'put') do |job|
|
85
|
+
if job
|
86
|
+
"moved to #{job.queue_name}"
|
87
|
+
else
|
88
|
+
"moved"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
puts 'Listening...'
|
94
|
+
|
95
|
+
Signal.trap("INT") do
|
96
|
+
puts 'Exiting'
|
97
|
+
Process.exit(0)
|
98
|
+
end
|
99
|
+
end
|
data/bin/qless-web
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
begin
|
5
|
+
require 'vegas'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'vegas'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'qless/server'
|
12
|
+
|
13
|
+
Vegas::Runner.new(Qless::Server, 'qless-web', {
|
14
|
+
:before_run => lambda {|v|
|
15
|
+
path = (ENV['RESQUECONFIG'] || v.args.first)
|
16
|
+
load path.to_s.strip if path
|
17
|
+
}
|
18
|
+
}) do |runner, opts, app|
|
19
|
+
# opts.on('-r redis-connection', "--redis redis-connection", "set the Redis connection string") {|redis_conf|
|
20
|
+
# runner.logger.info "Using Redis connection '#{redis_conf}'"
|
21
|
+
# Resque.redis = redis_conf
|
22
|
+
# }
|
23
|
+
end
|
data/lib/qless.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "redis"
|
3
|
+
require "json"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require "qless/version"
|
7
|
+
require "qless/config"
|
8
|
+
require "qless/queue"
|
9
|
+
require "qless/job"
|
10
|
+
require "qless/lua"
|
11
|
+
|
12
|
+
module Qless
|
13
|
+
extend self
|
14
|
+
|
15
|
+
def generate_jid
|
16
|
+
SecureRandom.uuid.gsub('-', '')
|
17
|
+
end
|
18
|
+
|
19
|
+
def stringify_hash_keys(hash)
|
20
|
+
hash.each_with_object({}) do |(key, value), result|
|
21
|
+
result[key.to_s] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# This is a unique identifier for the worker
|
26
|
+
def worker_name
|
27
|
+
@worker_name ||= [Socket.gethostname, Process.pid.to_s].join('-')
|
28
|
+
end
|
29
|
+
|
30
|
+
class UnsupportedRedisVersionError < StandardError; end
|
31
|
+
|
32
|
+
class ClientJobs
|
33
|
+
def initialize(client)
|
34
|
+
@client = client
|
35
|
+
end
|
36
|
+
|
37
|
+
def complete(offset=0, count=25)
|
38
|
+
@client._jobs.call([], ['complete', offset, count])
|
39
|
+
end
|
40
|
+
|
41
|
+
def tracked
|
42
|
+
results = JSON.parse(@client._track.call([], []))
|
43
|
+
results['jobs'] = results['jobs'].map { |j| Job.new(@client, j) }
|
44
|
+
results
|
45
|
+
end
|
46
|
+
|
47
|
+
def tagged(tag, offset=0, count=25)
|
48
|
+
JSON.parse(@client._tag.call([], ['get', tag, offset, count]))
|
49
|
+
end
|
50
|
+
|
51
|
+
def failed(t=nil, start=0, limit=25)
|
52
|
+
if not t
|
53
|
+
JSON.parse(@client._failed.call([], []))
|
54
|
+
else
|
55
|
+
results = JSON.parse(@client._failed.call([], [t, start, limit]))
|
56
|
+
results['jobs'] = results['jobs'].map { |j| Job.new(@client, j) }
|
57
|
+
results
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def [](id)
|
62
|
+
results = @client._get.call([], [id])
|
63
|
+
if results.nil?
|
64
|
+
results = @client._recur.call([], ['get', id])
|
65
|
+
if results.nil?
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
return RecurringJob.new(@client, JSON.parse(results))
|
69
|
+
end
|
70
|
+
Job.new(@client, JSON.parse(results))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ClientWorkers
|
75
|
+
def initialize(client)
|
76
|
+
@client = client
|
77
|
+
end
|
78
|
+
|
79
|
+
def counts
|
80
|
+
JSON.parse(@client._workers.call([], [Time.now.to_i]))
|
81
|
+
end
|
82
|
+
|
83
|
+
def [](name)
|
84
|
+
JSON.parse(@client._workers.call([], [Time.now.to_i, name]))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class ClientQueues
|
89
|
+
def initialize(client)
|
90
|
+
@client = client
|
91
|
+
end
|
92
|
+
|
93
|
+
def counts
|
94
|
+
JSON.parse(@client._queues.call([], [Time.now.to_i]))
|
95
|
+
end
|
96
|
+
|
97
|
+
def [](name)
|
98
|
+
Queue.new(name, @client)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ClientEvents
|
103
|
+
def initialize(redis)
|
104
|
+
@redis = redis
|
105
|
+
@actions = Hash.new()
|
106
|
+
end
|
107
|
+
|
108
|
+
def canceled(&block) ; @actions[:canceled ] = block; end
|
109
|
+
def completed(&block); @actions[:completed] = block; end
|
110
|
+
def failed(&block) ; @actions[:failed ] = block; end
|
111
|
+
def popped(&block) ; @actions[:popped ] = block; end
|
112
|
+
def stalled(&block) ; @actions[:stalled ] = block; end
|
113
|
+
def put(&block) ; @actions[:put ] = block; end
|
114
|
+
def track(&block) ; @actions[:track ] = block; end
|
115
|
+
def untrack(&block) ; @actions[:untrack ] = block; end
|
116
|
+
|
117
|
+
def listen
|
118
|
+
yield(self) if block_given?
|
119
|
+
@redis.subscribe(:canceled, :completed, :failed, :popped, :stalled, :put, :track, :untrack) do |on|
|
120
|
+
on.message do |channel, message|
|
121
|
+
callback = @actions[channel.to_sym]
|
122
|
+
if not callback.nil?
|
123
|
+
callback.call(message)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def stop
|
130
|
+
@redis.unsubscribe
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Client
|
135
|
+
# Lua scripts
|
136
|
+
attr_reader :_cancel, :_config, :_complete, :_fail, :_failed, :_get, :_heartbeat, :_jobs, :_peek, :_pop
|
137
|
+
attr_reader :_priority, :_put, :_queues, :_recur, :_retry, :_stats, :_tag, :_track, :_workers, :_depends
|
138
|
+
# A real object
|
139
|
+
attr_reader :config, :redis, :jobs, :queues, :workers
|
140
|
+
|
141
|
+
def initialize(options = {})
|
142
|
+
# This is the redis instance we're connected to
|
143
|
+
@redis = options[:redis] || Redis.connect(options) # use connect so REDIS_URL will be honored
|
144
|
+
@options = options
|
145
|
+
assert_minimum_redis_version("2.5.5")
|
146
|
+
@config = Config.new(self)
|
147
|
+
['cancel', 'config', 'complete', 'depends', 'fail', 'failed', 'get', 'heartbeat', 'jobs', 'peek', 'pop',
|
148
|
+
'priority', 'put', 'queues', 'recur', 'retry', 'stats', 'tag', 'track', 'workers'].each do |f|
|
149
|
+
self.instance_variable_set("@_#{f}", Lua.new(f, @redis))
|
150
|
+
end
|
151
|
+
|
152
|
+
@jobs = ClientJobs.new(self)
|
153
|
+
@queues = ClientQueues.new(self)
|
154
|
+
@workers = ClientWorkers.new(self)
|
155
|
+
end
|
156
|
+
|
157
|
+
def events
|
158
|
+
# Events needs its own redis instance of the same configuration, because
|
159
|
+
# once it's subscribed, we can only use pub-sub-like commands. This way,
|
160
|
+
# we still have access to the client in the normal case
|
161
|
+
@events ||= ClientEvents.new(Redis.connect(@options))
|
162
|
+
end
|
163
|
+
|
164
|
+
def track(jid)
|
165
|
+
@_track.call([], ['track', jid, Time.now.to_i])
|
166
|
+
end
|
167
|
+
|
168
|
+
def untrack(jid)
|
169
|
+
@_track.call([], ['untrack', jid, Time.now.to_i])
|
170
|
+
end
|
171
|
+
|
172
|
+
def tags(offset=0, count=100)
|
173
|
+
JSON.parse(@_tag.call([], ['top', offset, count]))
|
174
|
+
end
|
175
|
+
private
|
176
|
+
|
177
|
+
def assert_minimum_redis_version(version)
|
178
|
+
redis_version = @redis.info.fetch("redis_version")
|
179
|
+
return if Gem::Version.new(redis_version) >= Gem::Version.new(version)
|
180
|
+
|
181
|
+
raise UnsupportedRedisVersionError,
|
182
|
+
"You are running redis #{redis_version}, but qless requires at least #{version}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
data/lib/qless/config.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "qless/lua"
|
2
|
+
require "redis"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Qless
|
6
|
+
# A configuration class associated with a qless client
|
7
|
+
class Config
|
8
|
+
def initialize(client)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
@client._config.call([], ['get', key])
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
@client._config.call([], ['set', key, value])
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the specified `qless` configuration option, or if
|
21
|
+
# none is provided, get the complete current configuration
|
22
|
+
def all
|
23
|
+
return JSON.parse(@client._config.call([], ['get']))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Restore this option to the default (remove this option)
|
27
|
+
def clear(option)
|
28
|
+
@client._config.call([], ['unset', option])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|