sqew 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
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
+ tmp
16
+ test/tmp
17
+ test/version_tmp
18
+ spec/tmp/*
19
+ !spec/tmp/.gitkeep
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in smallq.gemspec
4
+ gemspec
5
+
6
+ gem "rake"
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Zach Moazeni
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # sqew (pronounced "skew")
2
+
3
+ sqew is a lightweight background processor. You start a single process that will act as a queue manager and will work multiple jobs concurrently. sqew is short for "small queue" and is not meant to be an all-in-one scalable solution. sqew adopts a similiar API to make migrating to other background processors easy.
4
+
5
+ ## When would sqew be a good fit for my project?
6
+
7
+ * You don't need to split workers across multiple machines.
8
+ * You don't want to manage multiple background worker processes, but you do want multiple jobs to run concurrently.
9
+ * You don't want to worry about threading issues.
10
+ * You don't want to worry about long running processes memory leaking.
11
+ * You don't care about the enqueueing or job forking performance.
12
+ * You don't need multiple queues (this may change soon).
13
+
14
+ If these don't fit the bill or you need more power, I recommend you try the great other great gems such as [Resque](https://github.com/defunkt/resque), [Sidekiq](https://github.com/mperham/sidekiq), and [Qu](https://github.com/bkeepers/qu).
15
+
16
+ ## Is it any good?
17
+
18
+ [Yes.](http://news.ycombinator.com/item?id=3067434)
19
+
20
+ ## Rails Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ gem sqew, :require "sqew/rails"
25
+
26
+ Add an initializer in `config/initializers/sqew.rb`
27
+
28
+ Sqew.configure do |config|
29
+ config.db = "#{Rails.root}/tmp/"
30
+ config.server = "http://0.0.0.0:8884"
31
+ end
32
+
33
+ The `db` config will be a directory where sqew will manage its databases, and the `server` config is what what the worker will connect at as well as where the application will post jobs to.
34
+
35
+ Once you have sqew configured, you can start the queue manager by running `rake sqew:work`. This will manage the queues, it'll act as a server where the application post jobs, and it will work the jobs as the arrive.
36
+
37
+ ## Installation
38
+
39
+ Add this line to your application's Gemfile:
40
+
41
+ gem sqew
42
+
43
+ And then execute:
44
+
45
+ $ bundle
46
+
47
+ Or install it yourself as:
48
+
49
+ $ gem install sqew
50
+
51
+ ## The Sqew Manager
52
+
53
+ The Sqew manager is a JSON API for inspecting the queue, pushing work onto the queue, and manipulating the queue and workers. Actions you can perform are:
54
+
55
+ # enqueue a job
56
+ # Sqew.push(TheJobClass, 1, 2, 3)
57
+ POST /enqueue
58
+ {"job":"TheJobClass", "args":[1, 2, 3]}
59
+
60
+ # ping the server to programatically see if it is alive
61
+ # Sqew.ping
62
+ GET /ping
63
+
64
+ # get the status of the queue, running jobs, failed jobs, and how many workers the server will use
65
+ # Sqew.status
66
+ GET /status
67
+
68
+ # dynamically change the number of workers (processes) the manager will use
69
+ # Sqew.workers = 10 (the default is 3)
70
+ PUT /workers
71
+ "10"
72
+
73
+ # clear the entire queue
74
+ # Sqew.clear
75
+ DELETE /clear
76
+
77
+ # clear just the failed jobs
78
+ # Sqew.clear("failed")
79
+ DELETE /clear
80
+ failed
81
+
82
+ # delete a specific job by id
83
+ # Sqew.delete(11)
84
+ DELETE /11
85
+
86
+ ## Enqueuing jobs
87
+
88
+ From your application you will create jobs just like [Resque](https://github.com/defunkt/resque), [Sidekiq](https://github.com/mperham/sidekiq), and [Qu](https://github.com/bkeepers/qu) in the form of
89
+
90
+ class MyJob
91
+ def perform(arg1, arg2)
92
+ # .. the job code
93
+ end
94
+ end
95
+
96
+ And the manager will receive the job and start working on it when it can. You can enqueue the job from Sqew:
97
+
98
+ Sqew.push(MyJob, 1, 2)
99
+
100
+ If you're using Rails 4 you can enqueue the job by using the Rails queuing API:
101
+
102
+ Rails.queue.push(MyJob, 1, 2)
103
+
104
+
105
+ ## TODO Soon
106
+
107
+ * Reusable God/Bluepil/Monit config for managing the worker
108
+ * Javascript browser front-end for the manager
109
+ * Multiple queues with weight (possibly)
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ task :default => :spec
data/ext/qu.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'qu/version'
2
+ require 'qu/logger'
3
+ require 'qu/failure'
4
+ require 'qu/payload'
5
+ require 'qu/backend/base'
6
+
7
+ require 'forwardable'
8
+ require 'logger'
9
+
10
+ module Qu
11
+ autoload :Worker, 'qu/worker'
12
+
13
+ extend SingleForwardable
14
+ extend self
15
+
16
+ attr_accessor :backend, :failure, :logger
17
+
18
+ def_delegators :backend, :length, :queues, :reserve, :clear, :connection=
19
+
20
+ def backend
21
+ @backend || raise("Qu backend not configured. Install one of the backend gems like qu-redis.")
22
+ end
23
+
24
+ def configure(&block)
25
+ block.call(self)
26
+ end
27
+
28
+ def enqueue(klass, *args)
29
+ backend.enqueue Payload.new(:klass => klass, :args => args)
30
+ end
31
+ end
32
+
33
+ Qu.configure do |c|
34
+ c.logger = Logger.new(STDOUT)
35
+ c.logger.level = Logger::INFO
36
+ end
data/ext/slave.rb ADDED
@@ -0,0 +1,14 @@
1
+ class Slave
2
+ def shutdown opts = {}
3
+ quiet = getopts(opts)['quiet']
4
+ raise "already shutdown" if @shutdown unless quiet
5
+ begin; @lifeline.cut; rescue Exception; end
6
+ @shutdown = true
7
+ end
8
+
9
+ class LifeLine
10
+ def cling &b
11
+ on_cut{ b.call if b }.join
12
+ end
13
+ end
14
+ end
data/lib/sqew.rb ADDED
@@ -0,0 +1,95 @@
1
+ require "slave"
2
+ require File.expand_path("../ext/slave", File.dirname(__FILE__))
3
+ require File.expand_path("../ext/qu", File.dirname(__FILE__))
4
+
5
+ require "leveldb"
6
+ require "sinatra/base"
7
+ require "thin"
8
+ require "multi_json"
9
+
10
+ require "sqew/version"
11
+ require "sqew/worker"
12
+ require "sqew/manager"
13
+ require "sqew/server"
14
+ require "sqew/payload"
15
+ require "sqew/backend/leveldb"
16
+
17
+ require "forwardable"
18
+ require "net/http"
19
+
20
+ module Sqew
21
+ module ClassMethods
22
+ extend Forwardable
23
+
24
+ attr_accessor :server
25
+
26
+ def qu
27
+ Qu
28
+ end
29
+
30
+ def server=(raw)
31
+ URI.parse(raw) # verify it's parsable
32
+ @server = raw
33
+ end
34
+
35
+ def_delegators :qu, :backend, :backend=, :length, :queues, :reserve, :logger, :logger=, :failure, :failure=
36
+ end
37
+ extend ClassMethods
38
+
39
+ class << self
40
+ def configure(*args, &block)
41
+ self.backend = Sqew::Backend::LevelDB.new
42
+ block.call(self)
43
+ self.server ||= "http://0.0.0.0:9962"
44
+ self.db ||= "/tmp/"
45
+ end
46
+
47
+ def http
48
+ uri = URI.parse(server)
49
+ @http ||= Net::HTTP.new(uri.host, uri.port)
50
+ end
51
+
52
+ def push(job, *args)
53
+ request = Net::HTTP::Post.new("/enqueue")
54
+ request.body = MultiJson.encode("job" => job.to_s, "args" => args)
55
+ http.request(request)
56
+ end
57
+ alias_method :enqueue, :push
58
+
59
+ def ping
60
+ request = Net::HTTP::Get.new("/ping")
61
+ http.request(request)
62
+ end
63
+
64
+ def status
65
+ request = Net::HTTP::Get.new("/status")
66
+ response = http.request(request)
67
+ if response.code == "200"
68
+ MultiJson.decode(http.request(request).body)
69
+ else
70
+ raise "Error connecting to server #{response.code}:#{response.body}"
71
+ end
72
+ end
73
+
74
+ def set_workers(count)
75
+ request = Net::HTTP::Put.new("/workers")
76
+ request.body = count.to_s
77
+ http.request(request)
78
+ end
79
+ alias_method :workers=, :set_workers
80
+
81
+ def clear(*queues)
82
+ request = Net::HTTP::Delete.new("/clear")
83
+ request.body = queues.join(",")
84
+ http.request(request)
85
+ end
86
+
87
+ def delete(id)
88
+ request = Net::HTTP::Delete.new("/#{id}")
89
+ http.request(request)
90
+ end
91
+ end
92
+
93
+ extend SingleForwardable
94
+ def_delegators :backend, :failed_jobs, :running_jobs, :queued_jobs, :db, :db=
95
+ end
@@ -0,0 +1,17 @@
1
+ require "qu/backend/immediate"
2
+
3
+ module Sqew
4
+ module Backend
5
+ class Immediate < Qu::Backend::Immediate
6
+ def queued_jobs
7
+ []
8
+ end
9
+ alias_method :running_jobs, :queued_jobs
10
+ alias_method :failed_jobs, :queued_jobs
11
+ end
12
+
13
+ def close
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,104 @@
1
+ module Sqew
2
+ module Backend
3
+ class LevelDB < Qu::Backend::Base
4
+ attr_accessor :db
5
+
6
+ def enqueue(payload)
7
+ id = Time.now.to_f.to_s
8
+ queue.put(id, MultiJson.encode(klass:payload.klass.to_s, args:payload.args), :sync => true)
9
+ end
10
+
11
+ def length(*)
12
+ queue.keys.length
13
+ end
14
+
15
+ def clear(*queues)
16
+ drop_all(queue) if queues.include?("queue") || queues.empty?
17
+ drop_all(errors) if queues.include?("failed") || queues.empty?
18
+ end
19
+
20
+ def clear_running
21
+ drop_all(running)
22
+ end
23
+
24
+ def delete(id)
25
+ if queue.exists?(id)
26
+ queue.delete(id)
27
+ elsif errors.exists?(id)
28
+ errors.delete(id)
29
+ end
30
+ end
31
+
32
+ def reserve(_, options = {block:false})
33
+ loop do
34
+ if raw = queue.first
35
+ id, job = raw
36
+ queue.delete(id, :sync => true)
37
+ running.put(id, job, :sync => true)
38
+ return Sqew::Payload.new(MultiJson.decode(job).update(id:id))
39
+ end
40
+
41
+ if options[:block]
42
+ sleep 3
43
+ else
44
+ break
45
+ end
46
+ end
47
+ end
48
+
49
+ def completed(payload)
50
+ running.delete(payload.id, :sync => true)
51
+ end
52
+
53
+ def failed(payload, error)
54
+ running.delete(payload.id, :sync => true)
55
+ errors.put(payload.id, MultiJson.encode("klass" => payload.klass.to_s, "args" => payload.args, "error" => error.message, "backtrace" => error.backtrace.join("\n")), :sync => true)
56
+ end
57
+
58
+ def failed_jobs
59
+ errors.to_a.map {|k,v| MultiJson.decode(v).update("id" => k) }
60
+ end
61
+
62
+ def running_jobs
63
+ running.to_a.map {|k,v| MultiJson.decode(v).update("id" => k) }
64
+ end
65
+
66
+ def queued_jobs
67
+ queue.to_a.map {|k,v| MultiJson.decode(v).update("id" => k) }
68
+ end
69
+
70
+ def release(*)
71
+ end
72
+
73
+ def register_worker(*)
74
+ end
75
+
76
+ def unregister_worker(*)
77
+ end
78
+
79
+ def close
80
+ queue.close
81
+ running.close
82
+ errors.close
83
+ @queue, @running, @errors = nil
84
+ end
85
+
86
+ private
87
+ def queue
88
+ @queue ||= ::LevelDB::DB.new("#{db}/queue.ldb")
89
+ end
90
+
91
+ def running
92
+ @running ||= ::LevelDB::DB.new("#{db}/running.ldb")
93
+ end
94
+
95
+ def errors
96
+ @errors ||= ::LevelDB::DB.new("#{db}/errors.ldb")
97
+ end
98
+
99
+ def drop_all(db)
100
+ db.each {|k,_| db.delete(k) }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,124 @@
1
+ module Sqew
2
+ class Manager < Qu::Worker
3
+ attr_accessor :max_workers
4
+
5
+ def initialize(max_workers = 3)
6
+ raise "Configure sqew before starting the manager" unless Sqew.server
7
+ super([])
8
+ @max_workers = max_workers
9
+ @uri = URI.parse(Sqew.server)
10
+ @poll = 1
11
+ @thin_server = nil
12
+
13
+ @group = ThreadGroup.new
14
+ end
15
+
16
+ def pause_workers
17
+ @paused_workers ||= @max_workers
18
+ @max_workers = 0
19
+ end
20
+
21
+ def resume_workers
22
+ if @paused_workers
23
+ @max_workers = @paused_workers
24
+ end
25
+ end
26
+
27
+ def max_workers=(count)
28
+ @paused_workers = nil
29
+ @max_workers = count
30
+ end
31
+
32
+ def start_server
33
+ logger.info "Starting server on #{@uri}"
34
+ Thread.new do
35
+ Thin::Logging.silent = true
36
+ @thin_server = Thin::Server.new(@uri.host, @uri.port, Server.new(self), {signals:false})
37
+ @thin_server.start
38
+ end
39
+ end
40
+
41
+ def stop_server
42
+ @thin_server.stop
43
+ end
44
+
45
+ def handle_signals
46
+ logger.debug "Worker #{id} registering traps for INT and TERM signals"
47
+ %W(INT TERM).each do |sig|
48
+ trap(sig) do
49
+ logger.info "Worker #{id} received #{sig}, will wait for workers to finish then quit"
50
+ @exiting = true
51
+ end
52
+ end
53
+ end
54
+
55
+ def work_off
56
+ Qu.backend.clear_running
57
+ super
58
+ end
59
+
60
+ def start
61
+ logger.warn "Worker #{id} starting"
62
+ start_slave
63
+ handle_signals
64
+ start_server
65
+ Qu.backend.clear_running
66
+ loop do
67
+ work
68
+ sleep @poll
69
+
70
+ if @exiting
71
+ stop_server
72
+ @group.list.map {|t| t.join }
73
+ Qu.backend.close
74
+ stop_slave
75
+ break
76
+ end
77
+ end
78
+ ensure
79
+ logger.debug "Worker #{id} done"
80
+ end
81
+
82
+ private
83
+ def work
84
+ if available?
85
+ job = Qu.reserve(self)
86
+ if job
87
+ thread = Thread.new do
88
+ begin
89
+ logger.debug "Worker #{id}:#{Process.pid} reserved job #{job}"
90
+ remote_thread = @worker.fork_job(job)
91
+ success, error = remote_thread.value
92
+ logger.debug "Worker #{id}:#{Process.pid} completed job #{job}"
93
+
94
+ if success
95
+ Qu.backend.completed(job)
96
+ else
97
+ Qu.failure.create(job, error) if Qu.failure
98
+ Qu.backend.failed(job, error)
99
+ end
100
+ rescue Exception => e
101
+ logger.error "Thread Error"
102
+ log_exception(e)
103
+ end
104
+ end
105
+ @group.add(thread)
106
+ end
107
+ end
108
+ end
109
+
110
+ def available?
111
+ @group.list.size < @max_workers
112
+ end
113
+
114
+ def start_slave
115
+ trap("INT") { }
116
+ @worker_server = Slave.new(:threadsafe => true) { Sqew::Worker.new }
117
+ @worker = @worker_server.object
118
+ end
119
+
120
+ def stop_slave
121
+ @worker_server.shutdown
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,14 @@
1
+ module Sqew
2
+ class Payload < Qu::Payload
3
+ def perform_forked(pipe)
4
+ klass.perform(*args)
5
+ pipe.write(Marshal.dump([true, nil]))
6
+ rescue Exception => e
7
+ pipe.write(Marshal.dump([false, e]))
8
+ raise e
9
+ # raise special exception
10
+ ensure
11
+ pipe.close
12
+ end
13
+ end
14
+ end
data/lib/sqew/rails.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "sqew"
2
+
3
+ Sqew.configure do |c|
4
+ c.logger = Logger.new(STDOUT)
5
+ c.logger.level = Logger::INFO
6
+ end
7
+
8
+ if defined?(Rails)
9
+ if defined?(Rails::Railtie)
10
+ require 'sqew/railtie'
11
+ else
12
+ Sqew.logger = Rails.logger
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module Sqew
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "sqew/tasks.rb"
5
+ end
6
+
7
+ initializer "sqew.logger" do |app|
8
+ Sqew.logger = Rails.logger
9
+ config.queue = Sqew if config.respond_to?(:queue)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,73 @@
1
+ module Sqew
2
+ class Server < Sinatra::Base
3
+
4
+ def initialize(manager)
5
+ super
6
+ @manager = manager
7
+ end
8
+
9
+ def route_missing
10
+ raise Sinatra::NotFound
11
+ end
12
+
13
+ set :show_exceptions, false
14
+
15
+ not_found do
16
+ [404, {}, ""]
17
+ end
18
+
19
+ post "/enqueue" do
20
+ begin
21
+ request.body.rewind
22
+ json = MultiJson.decode(request.body)
23
+ request.body.rewind
24
+
25
+ Qu.enqueue(json["job"], *json["args"])
26
+ [202, {"Content-Type" => "application/json"}, ""]
27
+ rescue Exception => e
28
+ json = MultiJson.encode({"error" => "#{e.message}\n#{e.backtrace}"})
29
+ [500, {"Content-Type" => "application/json"}, json]
30
+ end
31
+ end
32
+
33
+ get "/status" do
34
+ json = MultiJson.encode({
35
+ "queued" => Sqew.queued_jobs,
36
+ "failed" => Sqew.failed_jobs,
37
+ "running" => Sqew.running_jobs,
38
+ "workers" => @manager.max_workers
39
+ })
40
+ [200, {"Content-Type" => "application/json"}, json]
41
+ end
42
+
43
+ get "/ping" do
44
+ [200, {}, ""]
45
+ end
46
+
47
+ put "/workers" do
48
+ b = request.body.read
49
+ request.body.rewind
50
+ case b
51
+ when "pause"
52
+ @manager.pause_workers
53
+ when "resume"
54
+ @manager.resume_workers
55
+ else
56
+ @manager.max_workers = b.to_i
57
+ end
58
+ [200, {}, ""]
59
+ end
60
+
61
+ delete "/clear" do
62
+ b = request.body.read
63
+ request.body.rewind
64
+ Qu.clear(*b.split(","))
65
+ [200, {}, ""]
66
+ end
67
+
68
+ delete "/:id" do
69
+ Qu.backend.delete(params[:id])
70
+ [200, {}, ""]
71
+ end
72
+ end
73
+ end
data/lib/sqew/tasks.rb ADDED
@@ -0,0 +1,13 @@
1
+ namespace :sqew do
2
+ desc "Start a worker"
3
+ task :work => :environment do
4
+ Sqew::Manager.new.start
5
+ end
6
+ end
7
+
8
+ # Convenience tasks compatibility
9
+ task 'jobs:work' => 'sqew:work'
10
+ task 'resque:work' => 'sqew:work'
11
+
12
+ # No-op task in case it doesn't already exist
13
+ task :environment
@@ -0,0 +1,3 @@
1
+ module Sqew
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ module Sqew
2
+ class Worker
3
+ include Qu::Logger
4
+
5
+ def fork_job(job)
6
+ rd, wr = IO.pipe
7
+ pid = fork do
8
+ srand
9
+ rd.close
10
+ job.perform_forked(wr)
11
+ # catch special exception and regular/internalXS ones
12
+ # add logging to regular (internal)
13
+ end
14
+
15
+ Thread.new do
16
+ begin
17
+ Process.wait(pid)
18
+ wr.close
19
+ Marshal.load(rd.read)
20
+ ensure
21
+ rd.close
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/tasks.rb ADDED
@@ -0,0 +1,13 @@
1
+ namespace :sqew do
2
+ desc "Start a worker"
3
+ task :work => :environment do
4
+ Sqew::Manager.new.start
5
+ end
6
+ end
7
+
8
+ # Convenience tasks compatibility
9
+ task 'jobs:work' => 'sqew:work'
10
+ task 'resque:work' => 'sqew:work'
11
+
12
+ # No-op task in case it doesn't already exist
13
+ task :environment
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ describe Sqew::Backend::LevelDB do
4
+ before do
5
+ @backend = Sqew::Backend::LevelDB.new()
6
+ @backend.db = DB_PATH
7
+ end
8
+
9
+ it "should report queued jobs" do
10
+ Timecop.freeze(Time.utc(2012, 5, 2)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 1)) }
11
+ Timecop.freeze(Time.utc(2012, 5, 3)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 2)) }
12
+ Timecop.freeze(Time.utc(2012, 5, 1)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 3)) }
13
+ @backend.queued_jobs.should == [
14
+ {"klass"=>"TestJob", "args"=>3, "id" => "1335830400.0"},
15
+ {"klass"=>"TestJob", "args"=>1, "id" => "1335916800.0"},
16
+ {"klass"=>"TestJob", "args"=>2, "id" => "1336003200.0"}
17
+ ]
18
+ end
19
+
20
+ it "should report the failed jobs" do
21
+ error1 = double("error1", :message => "some error1", :backtrace => ["backtrace1"])
22
+ error2 = double("error2", :message => "some error2", :backtrace => ["backtrace2"])
23
+ error3 = double("error3", :message => "some error3", :backtrace => ["backtrace3"])
24
+
25
+ @backend.failed(Qu::Payload.new(:klass => TestJob, :args => 1, :id => "2"), error2)
26
+ @backend.failed(Qu::Payload.new(:klass => TestJob, :args => 1, :id => "3"), error3)
27
+ @backend.failed(Qu::Payload.new(:klass => TestJob, :args => 1, :id => "1"), error1)
28
+
29
+ @backend.failed_jobs.should == [
30
+ {"klass"=>"TestJob", "args"=>1, "id" => "1", "error" => "some error1", "backtrace" => "backtrace1"},
31
+ {"klass"=>"TestJob", "args"=>1, "id" => "2", "error" => "some error2", "backtrace" => "backtrace2"},
32
+ {"klass"=>"TestJob", "args"=>1, "id" => "3", "error" => "some error3", "backtrace" => "backtrace3"}
33
+ ]
34
+
35
+ end
36
+
37
+ it "should report running jobs" do
38
+ Timecop.freeze(Time.utc(2012, 5, 2)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 1)) }
39
+ Timecop.freeze(Time.utc(2012, 5, 3)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 2)) }
40
+
41
+ job = @backend.reserve(nil)
42
+ @backend.queued_jobs.should == [{"klass"=>"TestJob", "args"=>2, "id" => "1336003200.0"}]
43
+ @backend.running_jobs.should == [{"klass"=>"TestJob", "args"=>1, "id" => "1335916800.0"}]
44
+ end
45
+
46
+ it "should allow jobs to be deleted" do
47
+ error1 = double("error1", :message => "some error1", :backtrace => ["backtrace1"])
48
+ @backend.failed(Qu::Payload.new(:klass => TestJob, :args => 1, :id => "1"), error1)
49
+ Timecop.freeze(Time.utc(2012, 5, 2)) { @backend.enqueue(Qu::Payload.new(:klass => TestJob, :args => 1)) }
50
+
51
+ @backend.delete("1")
52
+ @backend.failed_jobs.should == []
53
+
54
+ @backend.delete("1335916800.0")
55
+ @backend.queued_jobs.should == []
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ require "spec_helper"
2
+
3
+ describe Sqew::Manager do
4
+ it "allows the server to be started and stopped" do
5
+ manager = Sqew::Manager.new(9962)
6
+ manager.start_server
7
+ sleep 1
8
+ response = Net::HTTP.get_response(URI.parse("http://0.0.0.0:9962/ping"))
9
+ response.code.should == "200"
10
+
11
+ manager.stop_server
12
+ sleep 1
13
+ expect { Net::HTTP.get_response(URI.parse("http://0.0.0.0:9962/ping")) }.to raise_error(SystemCallError)
14
+ end
15
+
16
+ it "allows queues to be cleared" do
17
+ Qu.enqueue(FailJob, -1)
18
+ manager = Sqew::Manager.new
19
+ manager.work_off
20
+ Qu.enqueue(TestJob, 5)
21
+ Qu.clear
22
+ Sqew.queued_jobs.should == []
23
+ Sqew.failed_jobs.should == []
24
+ end
25
+
26
+ it "allows queues to be cleared by name" do
27
+ Qu.enqueue(FailJob, -1)
28
+ manager = Sqew::Manager.new
29
+ manager.work_off
30
+ Qu.enqueue(TestJob, 5)
31
+ Qu.clear("failed")
32
+ Sqew.queued_jobs.should_not == []
33
+ Sqew.failed_jobs.should == []
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ require "spec_helper"
2
+
3
+ describe Sqew::Server do
4
+ before do
5
+ @old_backend = Sqew.backend
6
+ Sqew.backend = Sqew::Backend::Immediate.new
7
+
8
+ @manager = Sqew::Manager.new(3)
9
+ Artifice.activate_with(Sqew::Server.new(@manager))
10
+ end
11
+
12
+ after do
13
+ Artifice.deactivate
14
+ Sqew.backend = @old_backend
15
+ end
16
+
17
+ it "enqueues jobs" do
18
+ TestJob.testing.should == 0
19
+ Sqew.push("TestJob", 15)
20
+ TestJob.testing.should == 15
21
+ end
22
+
23
+ it "allows pings" do
24
+ response = Sqew.ping
25
+ response.code.should == "200"
26
+ end
27
+
28
+ it "returns the status" do
29
+ status = Sqew.status
30
+ status.should == {"queued" => [], "running" => [], "failed" => [], "workers" => 3}
31
+ end
32
+
33
+ it "allows workers to be configured" do
34
+ response = Sqew.set_workers(5)
35
+ response.code.should == "200"
36
+ @manager.max_workers.should == 5
37
+
38
+ Sqew.workers = 2
39
+ @manager.max_workers.should == 2
40
+ end
41
+
42
+ it "allows workers to be paused" do
43
+ Sqew.workers = :pause
44
+ @manager.max_workers.should == 0
45
+
46
+ Sqew.workers = :resume
47
+ @manager.max_workers.should == 3
48
+ end
49
+
50
+ it "should remember the number of workers, even if paused multiple times" do
51
+ Sqew.workers = :pause
52
+ Sqew.workers = :pause
53
+ Sqew.workers = :resume
54
+ @manager.max_workers.should == 3
55
+ end
56
+
57
+ it "should allow queues to be cleared" do
58
+ Qu.should_receive(:clear).with("queued", "failed")
59
+ Sqew.clear("queued", "failed")
60
+ end
61
+
62
+ it "should allow all queues to be cleared" do
63
+ Qu.should_receive(:clear).with()
64
+ Sqew.clear
65
+ end
66
+
67
+ it "should allow jobs to be deleted" do
68
+ Qu.backend.should_receive(:delete).with("10")
69
+ Sqew.delete("10")
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ require "rspec"
2
+ require "sqew"
3
+ require "fileutils"
4
+ require "timeout"
5
+ require "artifice"
6
+ require "timecop"
7
+ require "sqew/backend/immediate"
8
+
9
+ Dir[File.expand_path("support/*.rb", File.dirname(__FILE__))].each {|r| require r}
10
+
11
+ DB_PATH = File.expand_path("./tmp/db", File.dirname(__FILE__))
12
+
13
+ Sqew.configure do |c|
14
+ c.db = DB_PATH
15
+ c.logger.level = Logger::UNKNOWN
16
+ end
17
+
18
+ RSpec.configure do |c|
19
+ c.debug = true
20
+
21
+ c.before do
22
+ Sqew.backend.close
23
+ FileUtils.rm_rf(DB_PATH)
24
+ FileUtils.mkdir_p(DB_PATH)
25
+ TestJob.reset
26
+ Timecop.return
27
+ end
28
+ end
29
+
30
+ def save_and_open_page(response)
31
+ filename = File.expand_path("tmp/#{Digest::SHA1.hexdigest(Time.now.to_f.to_s)}.html", File.dirname(__FILE__))
32
+ File.open(filename, "w") do |file|
33
+ file << response.body
34
+ end
35
+ `open #{filename}`
36
+ end
data/spec/sqew_spec.rb ADDED
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe Sqew do
4
+ it "allows jobs to be enqueued and worked" do
5
+ TestJob.testing.should == 0
6
+ Qu.enqueue(TestJob, 20)
7
+ manager = Sqew::Manager.new
8
+ manager.work_off
9
+ TestJob.testing.should == 20
10
+ Sqew.running_jobs.should == []
11
+ end
12
+
13
+ it "reports the size of the queue" do
14
+ Sqew.length.should == 0
15
+ Qu.enqueue(TestJob, 20)
16
+ Sqew.length.should == 1
17
+ end
18
+
19
+ it "provides failed jobs" do
20
+ Qu.enqueue(FailJob, -1)
21
+ manager = Sqew::Manager.new
22
+ manager.work_off
23
+ failed = Sqew.failed_jobs
24
+ failed.size.should == 1
25
+ failed[0]["klass"].should == "FailJob"
26
+ end
27
+
28
+ it "provides running jobs" do
29
+ Qu.enqueue(SlowJob, 10)
30
+ manager = Sqew::Manager.new
31
+ begin
32
+ thread = Thread.new { manager.work_off }
33
+ sleep 1
34
+ running = Sqew.running_jobs
35
+ running.size.should == 1
36
+ running[0]["klass"].should == "SlowJob"
37
+ ensure
38
+ thread.terminate
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ class TestJob
2
+ @testing = 0
3
+
4
+ class << self
5
+ def perform(arg)
6
+ @testing = arg
7
+ end
8
+
9
+ def testing
10
+ @testing
11
+ end
12
+
13
+ def reset
14
+ @testing = 0
15
+ end
16
+ end
17
+ end
18
+
19
+ class FailJob
20
+ def self.perform(*)
21
+ raise "failed in FailJob"
22
+ end
23
+ end
24
+
25
+ class SlowJob
26
+ def self.perform(secs)
27
+ sleep secs
28
+ end
29
+ end
data/sqew.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sqew/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Zach Moazeni"]
6
+ gem.email = ["zach.moazeni@gmail.com"]
7
+ gem.description = "a lightweight background processor"
8
+ gem.summary = "sqew is a lightweight background processor. You start a single process that will act as a queue manager and will work multiple jobs concurrently.
9
+
10
+ sqew is not meant to be an all encompassing scalable solution. If you need more management over worker processes or need to split it among multiple machines, it's recommend to use other background processors such as resque, sidekiq, and qu"
11
+
12
+ gem.homepage = "https://github.com/zmoazeni/sqew"
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "sqew"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Sqew::VERSION
20
+
21
+ gem.add_dependency "leveldb-ruby", "~> 0.14"
22
+ gem.add_dependency "qu", "~> 0.1"
23
+ gem.add_dependency "multi_json", "~> 1.0"
24
+ gem.add_dependency "sinatra", "~> 1.0"
25
+ gem.add_dependency "thin", "~> 1.0"
26
+ gem.add_dependency "slave", "1.3"
27
+
28
+ gem.add_development_dependency "rspec"
29
+ gem.add_development_dependency "debugger"
30
+ gem.add_development_dependency "artifice"
31
+ gem.add_development_dependency "timecop"
32
+ end
metadata ADDED
@@ -0,0 +1,243 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqew
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Zach Moazeni
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: leveldb-ruby
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.14'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.14'
30
+ - !ruby/object:Gem::Dependency
31
+ name: qu
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.1'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: multi_json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sinatra
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: thin
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: slave
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - '='
100
+ - !ruby/object:Gem::Version
101
+ version: '1.3'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: '1.3'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: debugger
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: artifice
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: timecop
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description: a lightweight background processor
175
+ email:
176
+ - zach.moazeni@gmail.com
177
+ executables: []
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - .gitignore
182
+ - Gemfile
183
+ - LICENSE
184
+ - README.md
185
+ - Rakefile
186
+ - ext/qu.rb
187
+ - ext/slave.rb
188
+ - lib/sqew.rb
189
+ - lib/sqew/backend/immediate.rb
190
+ - lib/sqew/backend/leveldb.rb
191
+ - lib/sqew/manager.rb
192
+ - lib/sqew/payload.rb
193
+ - lib/sqew/rails.rb
194
+ - lib/sqew/railtie.rb
195
+ - lib/sqew/server.rb
196
+ - lib/sqew/tasks.rb
197
+ - lib/sqew/version.rb
198
+ - lib/sqew/worker.rb
199
+ - lib/tasks.rb
200
+ - spec/leveldb_spec.rb
201
+ - spec/manager_spec.rb
202
+ - spec/server_spec.rb
203
+ - spec/spec_helper.rb
204
+ - spec/sqew_spec.rb
205
+ - spec/support/jobs.rb
206
+ - spec/tmp/.gitkeep
207
+ - sqew.gemspec
208
+ homepage: https://github.com/zmoazeni/sqew
209
+ licenses: []
210
+ post_install_message:
211
+ rdoc_options: []
212
+ require_paths:
213
+ - lib
214
+ required_ruby_version: !ruby/object:Gem::Requirement
215
+ none: false
216
+ requirements:
217
+ - - ! '>='
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ required_rubygems_version: !ruby/object:Gem::Requirement
221
+ none: false
222
+ requirements:
223
+ - - ! '>='
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ requirements: []
227
+ rubyforge_project:
228
+ rubygems_version: 1.8.21
229
+ signing_key:
230
+ specification_version: 3
231
+ summary: sqew is a lightweight background processor. You start a single process that
232
+ will act as a queue manager and will work multiple jobs concurrently. sqew is not
233
+ meant to be an all encompassing scalable solution. If you need more management over
234
+ worker processes or need to split it among multiple machines, it's recommend to
235
+ use other background processors such as resque, sidekiq, and qu
236
+ test_files:
237
+ - spec/leveldb_spec.rb
238
+ - spec/manager_spec.rb
239
+ - spec/server_spec.rb
240
+ - spec/spec_helper.rb
241
+ - spec/sqew_spec.rb
242
+ - spec/support/jobs.rb
243
+ - spec/tmp/.gitkeep