qless 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Gemfile +8 -0
  2. data/HISTORY.md +168 -0
  3. data/README.md +571 -0
  4. data/Rakefile +28 -0
  5. data/bin/qless-campfire +106 -0
  6. data/bin/qless-growl +99 -0
  7. data/bin/qless-web +23 -0
  8. data/lib/qless.rb +185 -0
  9. data/lib/qless/config.rb +31 -0
  10. data/lib/qless/job.rb +259 -0
  11. data/lib/qless/job_reservers/ordered.rb +23 -0
  12. data/lib/qless/job_reservers/round_robin.rb +34 -0
  13. data/lib/qless/lua.rb +25 -0
  14. data/lib/qless/qless-core/cancel.lua +71 -0
  15. data/lib/qless/qless-core/complete.lua +218 -0
  16. data/lib/qless/qless-core/config.lua +44 -0
  17. data/lib/qless/qless-core/depends.lua +65 -0
  18. data/lib/qless/qless-core/fail.lua +107 -0
  19. data/lib/qless/qless-core/failed.lua +83 -0
  20. data/lib/qless/qless-core/get.lua +37 -0
  21. data/lib/qless/qless-core/heartbeat.lua +50 -0
  22. data/lib/qless/qless-core/jobs.lua +41 -0
  23. data/lib/qless/qless-core/peek.lua +155 -0
  24. data/lib/qless/qless-core/pop.lua +278 -0
  25. data/lib/qless/qless-core/priority.lua +32 -0
  26. data/lib/qless/qless-core/put.lua +156 -0
  27. data/lib/qless/qless-core/queues.lua +58 -0
  28. data/lib/qless/qless-core/recur.lua +181 -0
  29. data/lib/qless/qless-core/retry.lua +73 -0
  30. data/lib/qless/qless-core/ruby/lib/qless-core.rb +1 -0
  31. data/lib/qless/qless-core/ruby/lib/qless/core.rb +13 -0
  32. data/lib/qless/qless-core/ruby/lib/qless/core/version.rb +5 -0
  33. data/lib/qless/qless-core/ruby/spec/qless_core_spec.rb +13 -0
  34. data/lib/qless/qless-core/stats.lua +92 -0
  35. data/lib/qless/qless-core/tag.lua +100 -0
  36. data/lib/qless/qless-core/track.lua +79 -0
  37. data/lib/qless/qless-core/workers.lua +69 -0
  38. data/lib/qless/queue.rb +141 -0
  39. data/lib/qless/server.rb +411 -0
  40. data/lib/qless/tasks.rb +10 -0
  41. data/lib/qless/version.rb +3 -0
  42. data/lib/qless/worker.rb +195 -0
  43. 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'
@@ -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
@@ -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