eq 0.0.1 → 0.1.0
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/.gitignore +1 -0
- data/README.md +51 -11
- data/TODO.md +10 -0
- data/benchmarks/all.rb +13 -0
- data/benchmarks/parallel.rb +23 -0
- data/benchmarks/queue_backend_benchmark.rb +27 -0
- data/benchmarks/queueing.rb +13 -23
- data/benchmarks/working.rb +14 -25
- data/eq.gemspec +12 -2
- data/examples/queueing.rb +2 -2
- data/examples/scheduling.rb +19 -0
- data/examples/simple_usage.rb +20 -8
- data/examples/working.rb +2 -2
- data/lib/eq-queueing.rb +4 -13
- data/lib/eq-queueing/backends.rb +30 -1
- data/lib/eq-queueing/backends/leveldb.rb +232 -0
- data/lib/eq-queueing/backends/sequel.rb +34 -17
- data/lib/eq-queueing/queue.rb +26 -20
- data/lib/eq-scheduling.rb +33 -0
- data/lib/eq-scheduling/scheduler.rb +19 -0
- data/lib/eq-web.rb +5 -0
- data/lib/eq-web/server.rb +39 -0
- data/lib/eq-web/views/index.erb +45 -0
- data/lib/eq-working.rb +15 -7
- data/lib/eq-working/worker.rb +30 -3
- data/lib/eq.rb +39 -31
- data/lib/eq/boot/all.rb +1 -0
- data/lib/eq/boot/scheduling.rb +1 -0
- data/lib/eq/error.rb +4 -0
- data/lib/eq/job.rb +22 -16
- data/lib/eq/version.rb +1 -1
- data/log/.gitkeep +1 -0
- data/spec/lib/eq-queueing/backends/leveldb_spec.rb +32 -0
- data/spec/lib/eq-queueing/backends/sequel_spec.rb +5 -4
- data/spec/lib/eq-queueing/queue_spec.rb +27 -58
- data/spec/lib/eq-queueing_spec.rb +16 -0
- data/spec/lib/eq-scheduling_spec.rb +7 -0
- data/spec/lib/eq-working/worker_spec.rb +13 -0
- data/spec/lib/eq/job_spec.rb +16 -11
- data/spec/lib/eq_spec.rb +1 -1
- data/spec/mocks/a_job.rb +4 -0
- data/spec/mocks/a_unique_job.rb +6 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/shared_examples_for_queue.rb +60 -31
- metadata +80 -8
- data/lib/eq-working/manager.rb +0 -31
- data/lib/eq-working/system.rb +0 -10
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module EQ::Web
|
4
|
+
class Server < Sinatra::Base
|
5
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
6
|
+
set :views, "#{dir}/views"
|
7
|
+
|
8
|
+
get '/' do
|
9
|
+
erb :index
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/delete' do
|
13
|
+
EQ.queue.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/delete/:id' do
|
17
|
+
EQ.queue.pop(params[:id])
|
18
|
+
redirect url_path
|
19
|
+
end
|
20
|
+
|
21
|
+
helpers do
|
22
|
+
include Rack::Utils
|
23
|
+
alias_method :h, :escape_html
|
24
|
+
|
25
|
+
def current_page
|
26
|
+
url_path request.path_info.sub('/','')
|
27
|
+
end
|
28
|
+
|
29
|
+
def url_path(*path_parts)
|
30
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
31
|
+
end
|
32
|
+
alias_method :u, :url_path
|
33
|
+
|
34
|
+
def path_prefix
|
35
|
+
request.env['SCRIPT_NAME']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<% if EQ.worker && EQ.worker.alive? %>
|
2
|
+
<h2><%= EQ::Working.pool_size %> Worker</h2>
|
3
|
+
<% else %>
|
4
|
+
<h2>Worker deaktiviert</h2>
|
5
|
+
<% end %>
|
6
|
+
<% if EQ.scheduler && EQ.scheduler.alive? %>
|
7
|
+
<h2>Scheduler</h2>
|
8
|
+
<table>
|
9
|
+
<% EQ::Scheduling.events.each do |job_class, period| %>
|
10
|
+
<tr>
|
11
|
+
<td><%= job_class %></td>
|
12
|
+
<td><%= period %></td>
|
13
|
+
</tr>
|
14
|
+
<% end %>
|
15
|
+
</table>
|
16
|
+
<% else %>
|
17
|
+
<h2>Scheduler deaktiviert</h2>
|
18
|
+
<% end %>
|
19
|
+
<% if EQ.queue && EQ.queue.alive? %>
|
20
|
+
<h2><%= EQ.queue.count %> Queue Jobs (<a href='<%=u "/delete" %>'>Remove all</a>)</h2>
|
21
|
+
<table>
|
22
|
+
<thead>
|
23
|
+
<tr>
|
24
|
+
<th>Id</th>
|
25
|
+
<th>Queue</th>
|
26
|
+
<th>Payload</th>
|
27
|
+
<th>CreatedAt</th>
|
28
|
+
<th>StartedWorkingAt</th>
|
29
|
+
</tr>
|
30
|
+
</thead>
|
31
|
+
<tbody>
|
32
|
+
<% EQ.queue.iterator do |job| %>
|
33
|
+
<tr>
|
34
|
+
<td><%= job[:id] %> (<a href='<%=u "/delete/#{job[:id]}" %>'>Remove</a>)</td>
|
35
|
+
<td><%= job[:queue] %></td>
|
36
|
+
<td><%= job[:payload] %></td>
|
37
|
+
<td><%= job[:created_at] %></td>
|
38
|
+
<td><%= job[:started_working_at] %></td>
|
39
|
+
</tr>
|
40
|
+
<% end %>
|
41
|
+
</tbody>
|
42
|
+
</table>
|
43
|
+
<% else %>
|
44
|
+
<h2>Queue deaktiviert</h2>
|
45
|
+
<% end %>
|
data/lib/eq-working.rb
CHANGED
@@ -1,24 +1,32 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'eq')
|
2
2
|
require File.join(File.dirname(__FILE__), 'eq-working', 'worker')
|
3
|
-
require File.join(File.dirname(__FILE__), 'eq-working', 'manager')
|
4
|
-
require File.join(File.dirname(__FILE__), 'eq-working', 'system')
|
5
3
|
|
6
4
|
module EQ::Working
|
7
5
|
module_function
|
8
6
|
|
7
|
+
EQ_WORKER = :_eq_working
|
8
|
+
|
9
9
|
def boot
|
10
|
-
|
10
|
+
pool_size =EQ.config.worker_pool_size
|
11
|
+
case pool_size
|
12
|
+
when 0
|
13
|
+
puts "pool empty"
|
14
|
+
when 1
|
15
|
+
EQ::Working::Worker.supervise_as EQ_WORKER
|
16
|
+
else
|
17
|
+
Celluloid::Actor[EQ_WORKER] = EQ::Working::Worker.pool size: pool_size
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
def shutdown
|
14
|
-
worker.
|
22
|
+
worker.terminate! if worker
|
15
23
|
end
|
16
24
|
|
17
25
|
def worker
|
18
|
-
Celluloid::Actor[
|
26
|
+
Celluloid::Actor[EQ_WORKER]
|
19
27
|
end
|
20
28
|
|
21
|
-
def
|
22
|
-
|
29
|
+
def pool_size
|
30
|
+
EQ.config.worker_pool_size
|
23
31
|
end
|
24
32
|
end
|
data/lib/eq-working/worker.rb
CHANGED
@@ -3,8 +3,35 @@ module EQ::Working
|
|
3
3
|
include Celluloid
|
4
4
|
include EQ::Logging
|
5
5
|
|
6
|
-
def initialize
|
7
|
-
|
6
|
+
def initialize autostart=true
|
7
|
+
# start working async
|
8
|
+
process_jobs! if autostart
|
9
|
+
end
|
10
|
+
|
11
|
+
def process_jobs
|
12
|
+
sleep EQ.config.worker_delay
|
13
|
+
|
14
|
+
# TODO check if this is really what we want here, does it stop gracefully?
|
15
|
+
while Celluloid::Actor.current.alive?
|
16
|
+
if job = look_for_a_job
|
17
|
+
debug "got #{job.inspect}"
|
18
|
+
|
19
|
+
# this should happen in sync mode, because we don't want to pick
|
20
|
+
# too much jobs
|
21
|
+
process job
|
22
|
+
else
|
23
|
+
# currently no job
|
24
|
+
sleep 0.05
|
25
|
+
end
|
26
|
+
end
|
27
|
+
rescue Celluloid::DeadActorError
|
28
|
+
log_error 'dead'
|
29
|
+
sleep 0.05
|
30
|
+
retry
|
31
|
+
end
|
32
|
+
|
33
|
+
def look_for_a_job
|
34
|
+
EQ.queue.reserve if EQ.queue && EQ.queue.alive?
|
8
35
|
end
|
9
36
|
|
10
37
|
# @param [EQ::Job] job instance
|
@@ -12,7 +39,7 @@ module EQ::Working
|
|
12
39
|
def process job
|
13
40
|
debug "processing #{job.inspect}"
|
14
41
|
job.perform
|
15
|
-
EQ.queue.pop job.id
|
42
|
+
EQ.queue.pop! job.id
|
16
43
|
end
|
17
44
|
end
|
18
45
|
end
|
data/lib/eq.rb
CHANGED
@@ -1,20 +1,29 @@
|
|
1
|
+
# STDLIB
|
1
2
|
require 'ostruct'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
# rubygems
|
2
6
|
require 'celluloid'
|
3
7
|
|
4
8
|
require File.join(File.dirname(__FILE__), 'eq', 'version')
|
9
|
+
require File.join(File.dirname(__FILE__), 'eq', 'error')
|
5
10
|
require File.join(File.dirname(__FILE__), 'eq', 'logging')
|
6
11
|
require File.join(File.dirname(__FILE__), 'eq', 'job')
|
7
12
|
|
8
13
|
module EQ
|
14
|
+
extend SingleForwardable
|
15
|
+
|
9
16
|
class ConfigurationError < ArgumentError; end
|
10
17
|
|
11
18
|
DEFAULT_CONFIG = {
|
12
19
|
queue: 'sequel',
|
13
20
|
sequel: 'sqlite:/',
|
14
|
-
job_timeout: 5 # in seconds
|
21
|
+
job_timeout: 5, # in seconds
|
22
|
+
worker_pool_size: Celluloid.cores, # in threads
|
23
|
+
worker_delay: 0
|
15
24
|
}.freeze
|
16
25
|
|
17
|
-
|
26
|
+
module_function
|
18
27
|
|
19
28
|
def config
|
20
29
|
@config ||= OpenStruct.new DEFAULT_CONFIG
|
@@ -25,41 +34,40 @@ module EQ
|
|
25
34
|
# this boots queuing and working
|
26
35
|
# optional: to use another queuing or working subsystem just do
|
27
36
|
# require 'eq/working' or require 'eq/queueing' instead of require 'eq/all'
|
28
|
-
def boot
|
29
|
-
|
30
|
-
boot_working if defined? EQ::Working
|
31
|
-
end
|
37
|
+
def boot just=nil; manage :boot, just; end
|
38
|
+
def shutdown just=nil; manage :shutdown, just; end
|
32
39
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
def boot_queueing
|
39
|
-
EQ::Queueing.boot
|
40
|
-
end
|
40
|
+
def queue; EQ::Queueing.queue if queueing_loaded?; end
|
41
|
+
def worker; EQ::Working.worker if working_loaded?; end
|
42
|
+
def scheduler; EQ::Scheduling.scheduler if scheduling_loaded?; end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
+
# queue methods
|
45
|
+
%w[ jobs waiting working
|
46
|
+
push reserve pop
|
47
|
+
push! pop!
|
48
|
+
count ].each do |method_name|
|
49
|
+
def_delegator :queue, method_name
|
44
50
|
end
|
45
51
|
|
46
|
-
def
|
47
|
-
|
52
|
+
def alive?
|
53
|
+
alive = false
|
54
|
+
alive &= queue.alive? if queue
|
55
|
+
alive &= worker.alive? if worker
|
56
|
+
alive
|
48
57
|
end
|
49
58
|
|
50
|
-
def
|
51
|
-
EQ::Working.worker
|
52
|
-
end
|
53
|
-
|
54
|
-
def queueing?
|
55
|
-
queue.alive?
|
56
|
-
end
|
57
|
-
|
58
|
-
def working?
|
59
|
-
worker.alive?
|
60
|
-
end
|
59
|
+
def logger; Celluloid.logger; end
|
61
60
|
|
62
|
-
def
|
63
|
-
|
61
|
+
def queueing_loaded?; defined? EQ::Queueing; end
|
62
|
+
def working_loaded?; defined? EQ::Working; end
|
63
|
+
def scheduling_loaded?; defined? EQ::Scheduling; end
|
64
|
+
|
65
|
+
# @param [#to_s] action is the method name to execute on all parts
|
66
|
+
# @param [#to_s] specify just to execute the action on one part
|
67
|
+
def manage action, just=nil
|
68
|
+
what = just ? just.to_s : "queue work schedul"
|
69
|
+
EQ::Queueing.send(action) if what =~ /queue/ && queueing_loaded?
|
70
|
+
EQ::Working.send(action) if what =~ /work/ && working_loaded?
|
71
|
+
EQ::Scheduling.send(action) if what =~ /schedu/ && working_loaded?
|
64
72
|
end
|
65
73
|
end
|
data/lib/eq/boot/all.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'eq-scheduling')
|
data/lib/eq/error.rb
ADDED
data/lib/eq/job.rb
CHANGED
@@ -1,25 +1,31 @@
|
|
1
1
|
module EQ
|
2
|
-
class Job
|
3
|
-
class
|
4
|
-
|
5
|
-
Marshal.dump(unserialized_payload.flatten)
|
6
|
-
end
|
2
|
+
class Job
|
3
|
+
class UnknownJobClassError < EQ::Error; end
|
4
|
+
attr_reader :id, :queue, :payload
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# unmarshals the serialized_payload
|
14
|
-
def unpack
|
15
|
-
#[const_name.split("::").inject(Kernel){|res,current| res.const_get(current)}, *payload]
|
16
|
-
Marshal.load(serialized_payload)
|
6
|
+
def initialize id, queue, payload=nil
|
7
|
+
@id = id
|
8
|
+
@queue = queue.to_s
|
9
|
+
@payload = payload
|
17
10
|
end
|
18
11
|
|
19
12
|
# calls MyJobClass.perform(*payload)
|
20
13
|
def perform
|
21
|
-
|
22
|
-
|
14
|
+
job_class.perform *payload
|
15
|
+
end
|
16
|
+
|
17
|
+
def job_class
|
18
|
+
queue.split("::").inject(Kernel){|constant,part| constant.const_get(part)}
|
19
|
+
rescue NameError => e
|
20
|
+
raise UnknownJobClassError, e.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def unique?
|
24
|
+
config(:unique) == true
|
25
|
+
end
|
26
|
+
|
27
|
+
def config name
|
28
|
+
job_class.instance_variable_get "@#{name}"
|
23
29
|
end
|
24
30
|
end
|
25
31
|
end
|
data/lib/eq/version.rb
CHANGED
data/log/.gitkeep
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.gitkeep
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
EQ::Queueing::Backends.require_queue 'leveldb'
|
4
|
+
|
5
|
+
describe EQ::Queueing::Backends::LevelDB do
|
6
|
+
subject do
|
7
|
+
FileUtils.rm_rf 'tmp/rspec/queue.leveldb'
|
8
|
+
EQ::Queueing::Backends::LevelDB.new 'tmp/rspec/queue.leveldb'
|
9
|
+
end
|
10
|
+
it_behaves_like 'queue backend'
|
11
|
+
|
12
|
+
it 'persists created_at correctly' do
|
13
|
+
job_id = nil
|
14
|
+
created_at = Time.new(1986, 01, 01, 00, 00)
|
15
|
+
Timecop.freeze(created_at) do
|
16
|
+
job_id = subject.push EQ::Job.new(nil, AJob)
|
17
|
+
end
|
18
|
+
subject.jobs.find_created_at(job_id).should == created_at
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'persists started_working_at correctly' do
|
22
|
+
job_id = nil
|
23
|
+
Timecop.freeze(Time.new(1986, 01, 01, 00, 00, 0)) do
|
24
|
+
job_id = subject.push EQ::Job.new(nil, AJob)
|
25
|
+
end
|
26
|
+
started_working_at = Time.new(1986, 01, 01, 00, 01)
|
27
|
+
Timecop.freeze(started_working_at) do
|
28
|
+
subject.reserve
|
29
|
+
end
|
30
|
+
subject.jobs.find_started_working_at(job_id).should == started_working_at
|
31
|
+
end
|
32
|
+
end
|
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
EQ::Queueing::Backends.require_queue 'sequel'
|
4
|
+
|
3
5
|
describe EQ::Queueing::Backends::Sequel do
|
4
6
|
subject { EQ::Queueing::Backends::Sequel.new 'sqlite:/' }
|
5
|
-
it_behaves_like 'abstract queue'
|
6
7
|
it_behaves_like 'queue backend'
|
7
8
|
|
8
9
|
it 'handles ::Sequel::DatabaseError with retry' do
|
@@ -16,8 +17,8 @@ describe EQ::Queueing::Backends::Sequel do
|
|
16
17
|
raise ::Sequel::DatabaseError, "failed"
|
17
18
|
end
|
18
19
|
end
|
19
|
-
subject.
|
20
|
-
subject.push
|
21
|
-
subject.
|
20
|
+
subject.count(:waiting).should == 0
|
21
|
+
subject.push EQ::Job.new(nil, AJob)
|
22
|
+
subject.count(:waiting).should == 1
|
22
23
|
end
|
23
24
|
end
|
@@ -1,67 +1,36 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe EQ::Queueing::Queue do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
raise ArgumentError, "queue_backend mock only supports one waiting job at a time" if waiting
|
8
|
-
self.waiting = [1, payload]
|
9
|
-
1
|
10
|
-
end
|
11
|
-
|
12
|
-
def reserve
|
13
|
-
raise ArgumentError, "queue_backend mock only supports one working job at a time" if working
|
14
|
-
if self.working = waiting
|
15
|
-
self.working << Time.now
|
16
|
-
self.waiting = nil
|
17
|
-
return working
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def requeue_timed_out_jobs
|
22
|
-
raise ArgumentError, "queue_backend mock only supports on waiting job at a time" if waiting && working
|
23
|
-
# timeout after EQ.config.job_timeout seconds
|
24
|
-
if working && working.last <= (Time.now - EQ.config.job_timeout)
|
25
|
-
working.pop
|
26
|
-
self.waiting = working
|
27
|
-
self.working = nil
|
28
|
-
1
|
29
|
-
else
|
30
|
-
0
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def pop id
|
35
|
-
result = false
|
36
|
-
|
37
|
-
if waiting && id == waiting.first
|
38
|
-
self.waiting = nil
|
39
|
-
result = true
|
40
|
-
end
|
41
|
-
|
42
|
-
if working && id == working.first
|
43
|
-
self.working = nil
|
44
|
-
result = true
|
45
|
-
end
|
46
|
-
|
47
|
-
result
|
48
|
-
end
|
49
|
-
|
50
|
-
def waiting_count; waiting ? 1 : 0; end
|
51
|
-
def working_count; working ? 1 : 0; end
|
52
|
-
end.new
|
4
|
+
subject do
|
5
|
+
FileUtils.rm_rf 'tmp/rspec/queue.leveldb'
|
6
|
+
EQ::Queueing::Queue.new EQ::Queueing::Backends::LevelDB.new('tmp/rspec/queue.leveldb')
|
53
7
|
end
|
54
|
-
subject { EQ::Queueing::Queue.new(queue_backend) }
|
55
|
-
it_behaves_like 'abstract queue'
|
56
8
|
|
57
|
-
it '
|
58
|
-
|
59
|
-
|
9
|
+
it 'instantiates EQ::Job' do
|
10
|
+
id = nil
|
11
|
+
job_class = AJob
|
12
|
+
payload = ['bar', 'baz']
|
13
|
+
job = EQ::Job.new(id, job_class, payload)
|
14
|
+
EQ::Job.stub(:new).and_return(job)
|
15
|
+
EQ::Job.should_receive(:new).with(id, job_class, payload)
|
16
|
+
subject.push job_class, *payload
|
60
17
|
end
|
61
18
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
19
|
+
context 'unique jobs' do
|
20
|
+
it 'does not enqueue multiple times when args are the same' do
|
21
|
+
subject.count.should == 0
|
22
|
+
id = subject.push AUniqueJob, "foo"
|
23
|
+
subject.count.should == 1
|
24
|
+
id = subject.push AUniqueJob, "foo"
|
25
|
+
subject.count.should == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does enqueue multiple times when args differ' do
|
29
|
+
subject.count.should == 0
|
30
|
+
id = subject.push AUniqueJob, "foo"
|
31
|
+
subject.count.should == 1
|
32
|
+
id = subject.push AUniqueJob, "bar"
|
33
|
+
subject.count.should == 2
|
34
|
+
end
|
66
35
|
end
|
67
36
|
end
|