eq 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +1 -0
  2. data/README.md +51 -11
  3. data/TODO.md +10 -0
  4. data/benchmarks/all.rb +13 -0
  5. data/benchmarks/parallel.rb +23 -0
  6. data/benchmarks/queue_backend_benchmark.rb +27 -0
  7. data/benchmarks/queueing.rb +13 -23
  8. data/benchmarks/working.rb +14 -25
  9. data/eq.gemspec +12 -2
  10. data/examples/queueing.rb +2 -2
  11. data/examples/scheduling.rb +19 -0
  12. data/examples/simple_usage.rb +20 -8
  13. data/examples/working.rb +2 -2
  14. data/lib/eq-queueing.rb +4 -13
  15. data/lib/eq-queueing/backends.rb +30 -1
  16. data/lib/eq-queueing/backends/leveldb.rb +232 -0
  17. data/lib/eq-queueing/backends/sequel.rb +34 -17
  18. data/lib/eq-queueing/queue.rb +26 -20
  19. data/lib/eq-scheduling.rb +33 -0
  20. data/lib/eq-scheduling/scheduler.rb +19 -0
  21. data/lib/eq-web.rb +5 -0
  22. data/lib/eq-web/server.rb +39 -0
  23. data/lib/eq-web/views/index.erb +45 -0
  24. data/lib/eq-working.rb +15 -7
  25. data/lib/eq-working/worker.rb +30 -3
  26. data/lib/eq.rb +39 -31
  27. data/lib/eq/boot/all.rb +1 -0
  28. data/lib/eq/boot/scheduling.rb +1 -0
  29. data/lib/eq/error.rb +4 -0
  30. data/lib/eq/job.rb +22 -16
  31. data/lib/eq/version.rb +1 -1
  32. data/log/.gitkeep +1 -0
  33. data/spec/lib/eq-queueing/backends/leveldb_spec.rb +32 -0
  34. data/spec/lib/eq-queueing/backends/sequel_spec.rb +5 -4
  35. data/spec/lib/eq-queueing/queue_spec.rb +27 -58
  36. data/spec/lib/eq-queueing_spec.rb +16 -0
  37. data/spec/lib/eq-scheduling_spec.rb +7 -0
  38. data/spec/lib/eq-working/worker_spec.rb +13 -0
  39. data/spec/lib/eq/job_spec.rb +16 -11
  40. data/spec/lib/eq_spec.rb +1 -1
  41. data/spec/mocks/a_job.rb +4 -0
  42. data/spec/mocks/a_unique_job.rb +6 -0
  43. data/spec/spec_helper.rb +12 -0
  44. data/spec/support/shared_examples_for_queue.rb +60 -31
  45. metadata +80 -8
  46. data/lib/eq-working/manager.rb +0 -31
  47. 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 %>
@@ -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
- Celluloid::Actor[:_eq_working] = EQ::Working::System.run!
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.finalize! if worker
22
+ worker.terminate! if worker
15
23
  end
16
24
 
17
25
  def worker
18
- Celluloid::Actor[:_eq_working]
26
+ Celluloid::Actor[EQ_WORKER]
19
27
  end
20
28
 
21
- def worker_pool
22
- Celluloid::Actor[:_eq_working_pool]
29
+ def pool_size
30
+ EQ.config.worker_pool_size
23
31
  end
24
32
  end
@@ -3,8 +3,35 @@ module EQ::Working
3
3
  include Celluloid
4
4
  include EQ::Logging
5
5
 
6
- def initialize
7
- debug "initialized worker"
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
- module_function
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
- boot_queueing if defined? EQ::Queueing
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 shutdown
34
- EQ::Working.shutdown if defined? EQ::Working
35
- EQ::Queueing.shutdown if defined? EQ::Queueing
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
- def boot_working
43
- EQ::Working.boot
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 queue
47
- EQ::Queueing.queue
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 worker
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 logger
63
- Celluloid.logger
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
@@ -1,2 +1,3 @@
1
1
  require File.join(File.dirname(__FILE__), 'queueing')
2
+ require File.join(File.dirname(__FILE__), 'scheduling')
2
3
  require File.join(File.dirname(__FILE__), 'working')
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'eq-scheduling')
@@ -0,0 +1,4 @@
1
+ module EQ
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -1,25 +1,31 @@
1
1
  module EQ
2
- class Job < Struct.new(:id, :serialized_payload)
3
- class << self
4
- def dump *unserialized_payload
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
- def load id, serialized_payload
9
- Job.new id, serialized_payload
10
- end
11
- end
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
- const, *payload = unpack
22
- const.perform *payload
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
@@ -1,3 +1,3 @@
1
1
  module EQ
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -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.waiting_count.should == 0
20
- subject.push "foo"
21
- subject.waiting_count.should == 1
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
- let(:queue_backend) do
5
- Class.new(Struct.new(:waiting, :working)) do
6
- def push payload
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 'serializes jobs' do
58
- EQ::Job.should_receive(:dump).with(["foo"])
59
- subject.push "foo"
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
- it 'deserializes jobs' do
63
- subject.push "foo"
64
- EQ::Job.should_receive(:load).with(1, EQ::Job.dump(["foo"]))
65
- subject.reserve
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