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