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.
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