qless 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|