eq 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in eq.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara request specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jens Bissinger
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,79 @@
1
+ # EXPERIMENTAL FOO, THE DEVILS RIDE...
2
+
3
+ # EQ - Embedded Queueing
4
+
5
+ EQ is a little framework to queue tasks within a process.
6
+
7
+ [![Travis-CI Build Status](https://secure.travis-ci.org/dpree/eq.png)](https://secure.travis-ci.org/dpree/eq)
8
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/dpree/eq)
9
+
10
+ ## Installation
11
+
12
+ Install it yourself using Rubygems.
13
+
14
+ $ gem install eq
15
+
16
+ Or use something like [Bundler](http://gembundler.com/).
17
+
18
+ ## Example
19
+
20
+ If you want to execute a simple example you can just run [examples/simple_usage.rb](./examples/simple_usage.rb) from your commandline.
21
+
22
+ **1. Define a Job class with a perform method.**
23
+
24
+ class MyJob
25
+ def self.perform *some_args
26
+ # do some long running stuff here
27
+ end
28
+ end
29
+
30
+ **2. Start the EQ system.**
31
+
32
+ EQ.boot
33
+
34
+ **3. Let EQ do some work you.**
35
+
36
+ EQ.queue.push MyJob, 'foo'
37
+ EQ.queue.push MyJob, 'bar'
38
+
39
+
40
+ ## Configuration
41
+
42
+ Right now there is only one queueing backend available that is based on the Sequel gem. Therefore, basically any SQL database supported by Sequel might be used.
43
+
44
+ The default SQL database that is used is a . You can change it using any argument that Sequel.connect method would accept.
45
+
46
+ # SQLite3 in-memory (default) using String syntax
47
+ EQ.config.sequel = 'sqlite:/'
48
+
49
+ # SQLite3 file using Hash syntax
50
+ EQ.config.sequel = {adapter: 'sqlite', database: 'my_db.sqlite3'}
51
+
52
+ # Postgres
53
+ EQ.config.sequel = 'postgres://user:password@host:port/my_db'
54
+
55
+
56
+ ### Logging
57
+
58
+ EQ uses the logging mechanism of the underlying Celluloid (`Celluloid.logger`) framework. Basically you can just bind it to your application logger or re-configure it (see the Documentation of the `Logger` class from Ruby Standard Library).
59
+
60
+ **Changing the Logger:**
61
+
62
+ # Use the logger of your Rails application.
63
+ Celluloid.logger = Rails.logger
64
+
65
+ # No more logging at all.
66
+ Celluloid.logger = Logger.new('/dev/null')
67
+
68
+ ## Contributing
69
+
70
+ 1. Fork it
71
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
72
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
73
+ 4. Push to the branch (`git push origin my-new-feature`)
74
+ 5. Create new Pull Request
75
+
76
+ # LICENSE
77
+
78
+ Copyright (c) 2012 Jens Bissinger. See [LICENSE](LICENSE).
79
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'all')
4
+
5
+ require 'benchmark'
6
+ require 'tempfile'
7
+
8
+ EQ.logger.level = Logger::Severity::ERROR
9
+
10
+ class MyJob
11
+ def self.perform stuff
12
+ end
13
+ end
14
+
15
+ n = 1000
16
+ Benchmark.bm(50) do |b|
17
+ EQ.boot
18
+ b.report('memory-based sqlite') do
19
+ n.times { |i| EQ.queue.push! MyJob, i }
20
+ EQ.queue.waiting_count # block
21
+ end
22
+ EQ.shutdown
23
+
24
+ EQ.config do |config|
25
+ config[:sqlite] = Tempfile.new('').path
26
+ end
27
+ EQ.boot
28
+ b.report('file-based sqlite') do
29
+ n.times { |i| EQ.queue.push! MyJob, i }
30
+ EQ.queue.waiting_count # block
31
+ end
32
+ EQ.shutdown
33
+ end
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'all')
4
+
5
+ require 'benchmark'
6
+ require 'tempfile'
7
+
8
+ EQ.logger.level = Logger::Severity::ERROR
9
+
10
+ class MyJob
11
+ def self.perform stuff
12
+ end
13
+ end
14
+
15
+ n = 500
16
+ Benchmark.bm(50) do |b|
17
+ EQ.boot_queueing
18
+ n.times { |i| EQ.queue.push! MyJob, i }
19
+ b.report('memory-based sqlite') do
20
+ EQ.boot_working
21
+ sleep 0.01 until EQ.queue.waiting_count == 0
22
+ end
23
+ EQ.shutdown
24
+
25
+ EQ.config do |config|
26
+ config[:sqlite] = Tempfile.new('').path
27
+ end
28
+ EQ.boot_queueing
29
+ n.times { |i| EQ.queue.push! MyJob, i }
30
+ b.report('file-based sqlite') do
31
+ EQ.boot_working
32
+ sleep 0.01 until EQ.queue.waiting_count == 0
33
+ end
34
+ EQ.shutdown
35
+ end
data/eq.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/eq/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jens Bissinger"]
6
+ gem.email = ["mail@jens-bissinger.de"]
7
+ gem.description = %q{Embedded Queueing. Background processing within a single process using multi-threading and a SQL database.}
8
+ gem.summary = %q{Based on Celluloid (multi-threading) and Sequel (SQLite3, MySQL, PostgreSQL, ...).}
9
+ gem.homepage = "https://github.com/dpree/eq"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "eq"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = EQ::VERSION
17
+
18
+ gem.add_dependency "sqlite3"
19
+ gem.add_dependency "sequel"
20
+ gem.add_dependency "celluloid"
21
+ gem.add_development_dependency "guard"
22
+ gem.add_development_dependency "guard-rspec"
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "rake"
25
+ gem.add_development_dependency "timecop"
26
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'queueing')
3
+
4
+ EQ.logger.level = Logger::Severity::INFO
5
+ EQ.config do |config|
6
+ config[:sqlite] = "foo.sqlite"
7
+ end
8
+
9
+ def say words; EQ.logger.info(words); end
10
+
11
+ class SingleJob
12
+ def self.perform
13
+ sleep 0.05
14
+ end
15
+ end
16
+
17
+ require 'timeout'
18
+ begin
19
+ Timeout.timeout(120) do
20
+ EQ.boot
21
+ loop do
22
+ say "pushed!"
23
+ sleep 0.05
24
+ EQ.queue.push! SingleJob
25
+ end
26
+ end
27
+ rescue Timeout::Error
28
+ say "shutdown: #{EQ.shutdown}"
29
+ end
30
+
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'all')
3
+
4
+ # Define a Job class with a perform method.
5
+ class MyJob
6
+ RESULT_PATH = 'my_job_result.txt'
7
+
8
+ def self.perform enqueued_at, workload_in_seconds
9
+ # do some long running stuff here
10
+ sleep workload_in_seconds
11
+ File.open RESULT_PATH, 'a' do |file|
12
+ finished_at = Time.now
13
+ file.puts "Processed a job with workload of #{workload_in_seconds}s:\n"\
14
+ " - enqueued at = #{enqueued_at}\n"\
15
+ " - finished at = #{finished_at}\n"\
16
+ " - actual workload = #{finished_at - enqueued_at}s"
17
+ end
18
+ end
19
+ end
20
+
21
+ # Cleanup results file.
22
+ File.delete MyJob::RESULT_PATH if File.exists? MyJob::RESULT_PATH
23
+
24
+ # Start the EQ system.
25
+ EQ.boot
26
+
27
+ # Enqueue some work.
28
+ EQ.queue.push MyJob, Time.now, 1
29
+ EQ.queue.push MyJob, Time.now, 2
30
+
31
+ # Wait some time to get the work done.
32
+ sleep 3
33
+
34
+ # Read the results file.
35
+ puts File.read MyJob::RESULT_PATH
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'all')
3
+
4
+ EQ.logger.level = Logger::Severity::INFO
5
+ EQ.config do |config|
6
+ config[:sqlite] = "foo.sqlite"
7
+ end
8
+
9
+ def say words; EQ.logger.info(words); end
10
+
11
+ class SingleJob
12
+ def self.perform
13
+ say "worked!"
14
+ sleep 0.05
15
+ end
16
+ end
17
+
18
+ require 'timeout'
19
+ begin
20
+ Timeout.timeout(120) do
21
+ EQ.boot
22
+ sleep 0.5 while EQ.working?
23
+ end
24
+ rescue Timeout::Error
25
+ say "shutdown: #{EQ.shutdown}"
26
+ end
27
+
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), 'queueing')
2
+ require File.join(File.dirname(__FILE__), 'working')
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'eq-queueing')
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'eq-working')
data/lib/eq/job.rb ADDED
@@ -0,0 +1,25 @@
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
7
+
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)
17
+ end
18
+
19
+ # calls MyJobClass.perform(*payload)
20
+ def perform
21
+ const, *payload = unpack
22
+ const.perform *payload
23
+ end
24
+ end
25
+ end
data/lib/eq/logging.rb ADDED
@@ -0,0 +1,15 @@
1
+ module EQ
2
+ module Logging
3
+ def debug message
4
+ EQ.logger.debug message
5
+ end
6
+
7
+ def info message
8
+ EQ.logger.info message
9
+ end
10
+
11
+ def log_error message
12
+ EQ.logger.error message
13
+ end
14
+ end
15
+ end
data/lib/eq/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module EQ
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,133 @@
1
+ require 'sequel'
2
+
3
+ module EQ::Queueing::Backends
4
+
5
+ # this class provides a queueing backend via Sequel ORM mapper
6
+ # basically any database adapter known by Sequel is supported
7
+ # configure via EQ::conig[:sequel]
8
+ class Sequel
9
+ include EQ::Logging
10
+
11
+ TABLE_NAME = :jobs
12
+
13
+ attr_reader :db
14
+
15
+ # establishes the connection to the database and ensures that
16
+ # the jobs table is created
17
+ def initialize config
18
+ connect config
19
+ create_table_if_not_exists!
20
+ end
21
+
22
+ # @param [#to_sequel_block] payload
23
+ # @return [Fixnum] id of the job
24
+ def push payload
25
+ jobs.insert payload: payload.to_sequel_blob, created_at: Time.now
26
+ rescue ::Sequel::DatabaseError => e
27
+ retry if on_error e
28
+ end
29
+
30
+ # pulls a job from the waiting stack and moves it to the
31
+ # working stack. sets a timestamp :started_working_at so that
32
+ # the working duration can be tracked.
33
+ # @param [Time] now
34
+ # @return [Array<Fixnum, String>] job data consisting of id and payload
35
+ def reserve
36
+ db.transaction do
37
+ if job = waiting.order(:id.asc).limit(1).first
38
+ job[:started_working_at] = Time.now
39
+ update_job!(job)
40
+ [job[:id], job[:payload]]
41
+ end
42
+ end
43
+ rescue ::Sequel::DatabaseError => e
44
+ retry if on_error e
45
+ end
46
+
47
+ # finishes a job in the working queue
48
+ # @param [Fixnum] id of the job
49
+ # @return [TrueClass, FalseClass] true, when there was a job that could be deleted
50
+ def pop id
51
+ jobs.where(id: id).delete == 1
52
+ rescue ::Sequel::DatabaseError => e
53
+ retry if on_error e
54
+ end
55
+
56
+ # list of jobs waiting to be worked on
57
+ def waiting
58
+ jobs.where(started_working_at: nil)
59
+ rescue ::Sequel::DatabaseError => e
60
+ retry if on_error e
61
+ end
62
+
63
+ # list of jobs currentyl being worked on
64
+ def working
65
+ waiting.invert
66
+ rescue ::Sequel::DatabaseError => e
67
+ retry if on_error e
68
+ end
69
+
70
+ # list of all jobs
71
+ def jobs
72
+ db[TABLE_NAME]
73
+ rescue ::Sequel::DatabaseError => e
74
+ retry if on_error e
75
+ end
76
+
77
+ # updates a changed job object, uses the :id key to identify the job
78
+ # @param [Hash] changed job
79
+ def update_job! changed_job
80
+ jobs.where(id: changed_job[:id]).update(changed_job)
81
+ rescue ::Sequel::DatabaseError => e
82
+ retry if on_error e
83
+ end
84
+
85
+ # this re-enqueues jobs that timed out
86
+ # @return [Fixnum] number of jobs that were re-enqueued
87
+ def requeue_timed_out_jobs
88
+ # 10 seconds ago
89
+ jobs.where{started_working_at <= (Time.now - EQ.config.job_timeout)}\
90
+ .update(started_working_at: nil)
91
+ end
92
+
93
+ # statistics:
94
+ # - #job_count
95
+ # - #working_count
96
+ # - #waiting_count
97
+ %w[ job working waiting ].each do |stats_name|
98
+ define_method "#{stats_name}_count" do
99
+ begin
100
+ send(stats_name).send(:count)
101
+ rescue ::Sequel::DatabaseError => e
102
+ retry if on_error e
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ # connects to the given database config
110
+ def connect config
111
+ @db = ::Sequel.connect config
112
+ rescue ::Sequel::DatabaseError => e
113
+ retry if on_error e
114
+ end
115
+
116
+ def create_table_if_not_exists!
117
+ db.create_table? TABLE_NAME do
118
+ primary_key :id
119
+ Timestamp :created_at
120
+ Timestamp :started_working_at
121
+ Blob :payload
122
+ end
123
+ rescue ::Sequel::DatabaseError => e
124
+ retry if on_error e
125
+ end
126
+
127
+ def on_error error
128
+ log_error error.inspect
129
+ sleep 0.05
130
+ true
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,6 @@
1
+ module EQ::Queueing
2
+ module Backends
3
+ end
4
+ end
5
+
6
+ require File.join(File.dirname(__FILE__), 'backends', 'sequel')
@@ -0,0 +1,54 @@
1
+ module EQ::Queueing
2
+
3
+ # this class basically provides a API that wraps the low-level calls
4
+ # to the queueing backend that is configured and passed to the #initialize method
5
+ # furthermore this class adds some functionality to serialize / deserialze
6
+ # using the Job class
7
+ class Queue
8
+ include Celluloid
9
+ include EQ::Logging
10
+
11
+ %w[ job_count waiting_count working_count waiting working ].each do |method_name|
12
+ define_method method_name do
13
+ queue.send(method_name)
14
+ end
15
+ end
16
+ alias :size :job_count
17
+
18
+ # @param [Object] queue_backend
19
+ def initialize queue_backend
20
+ @queue = queue_backend
21
+ end
22
+
23
+ # @param [Array<Class, *payload>] unserialized_payload
24
+ # @return [Fixnum] job_id
25
+ def push *unserialized_payload
26
+ debug "enqueing #{unserialized_payload.inspect} ..."
27
+ queue.push EQ::Job.dump(unserialized_payload)
28
+ end
29
+
30
+ # @return [EQ::Job, nilClass] job instance
31
+ def reserve
32
+ requeue_timed_out_jobs
33
+ if serialized_job = queue.reserve
34
+ job_id, serialized_payload = *serialized_job
35
+ job = EQ::Job.load job_id, serialized_payload
36
+ debug "dequeud #{job.inspect}"
37
+ job
38
+ end
39
+ end
40
+
41
+ # @return [TrueClass, FalseClass]
42
+ def pop job_id
43
+ queue.pop job_id
44
+ end
45
+
46
+ # re-enqueues jobs that timed out
47
+ def requeue_timed_out_jobs
48
+ queue.requeue_timed_out_jobs
49
+ end
50
+
51
+ attr_reader :queue
52
+
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ require File.join(File.dirname(__FILE__), 'eq')
2
+ require File.join(File.dirname(__FILE__), 'eq-queueing', 'backends')
3
+ require File.join(File.dirname(__FILE__), 'eq-queueing', 'queue')
4
+
5
+ module EQ::Queueing
6
+ module_function
7
+
8
+ def boot
9
+ EQ::Queueing::Queue.supervise_as :_eq_queueing, initialize_queueing_backend
10
+ end
11
+
12
+ def shutdown
13
+ queue.terminate! if queue
14
+ end
15
+
16
+ def queue
17
+ Celluloid::Actor[:_eq_queueing]
18
+ end
19
+
20
+ # @raise ConfigurationError when EQ.config.queue is not supported
21
+ def initialize_queueing_backend
22
+ queue_config = EQ.config.send(EQ.config.queue)
23
+ case EQ.config.queue
24
+ when 'sequel'
25
+ EQ::Queueing::Backends::Sequel.new queue_config
26
+ else
27
+ raise EQ::ConfigurationError, "EQ.config.queue = '#{EQ.config.queue}' is not supported!"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ module EQ::Working
2
+ class Manager
3
+ include Celluloid
4
+ include EQ::Logging
5
+
6
+ def initialize
7
+ run!
8
+ end
9
+
10
+ # polls the EQ.queue via EQ.queue.reserve
11
+ def run
12
+ debug "worker manager running"
13
+ loop do
14
+ if EQ.queue && job = EQ.queue.reserve
15
+ debug "got #{job.inspect}"
16
+ if worker = EQ::Working.worker_pool
17
+ debug ' - found worker'
18
+ worker.process! job
19
+ else
20
+ debug ' - no worker'
21
+ end
22
+ else
23
+ # currently no job
24
+ end
25
+ sleep 0.01
26
+ end
27
+ rescue Celluloid::DeadActorError
28
+ retry
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ module EQ::Working
2
+ class System < Celluloid::SupervisionGroup
3
+ include EQ::Logging
4
+
5
+ # TODO celluloid: replace this with the following in the next version
6
+ # pool Worker, as: _eq_working_pool
7
+ supervise EQ::Working::Worker, as: :_eq_working_pool, method: 'pool_link'
8
+ supervise EQ::Working::Manager, as: :_eq_working_manager
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module EQ::Working
2
+ class Worker
3
+ include Celluloid
4
+ include EQ::Logging
5
+
6
+ def initialize
7
+ debug "initialized worker"
8
+ end
9
+
10
+ # @param [EQ::Job] job instance
11
+ # @return [TrueClass, FalseClass] true when job is done and deleted
12
+ def process job
13
+ debug "processing #{job.inspect}"
14
+ job.perform
15
+ EQ.queue.pop job.id
16
+ end
17
+ end
18
+ end
data/lib/eq-working.rb ADDED
@@ -0,0 +1,24 @@
1
+ require File.join(File.dirname(__FILE__), 'eq')
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
+
6
+ module EQ::Working
7
+ module_function
8
+
9
+ def boot
10
+ Celluloid::Actor[:_eq_working] = EQ::Working::System.run!
11
+ end
12
+
13
+ def shutdown
14
+ worker.finalize! if worker
15
+ end
16
+
17
+ def worker
18
+ Celluloid::Actor[:_eq_working]
19
+ end
20
+
21
+ def worker_pool
22
+ Celluloid::Actor[:_eq_working_pool]
23
+ end
24
+ end
data/lib/eq.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'ostruct'
2
+ require 'celluloid'
3
+
4
+ require File.join(File.dirname(__FILE__), 'eq', 'version')
5
+ require File.join(File.dirname(__FILE__), 'eq', 'logging')
6
+ require File.join(File.dirname(__FILE__), 'eq', 'job')
7
+
8
+ module EQ
9
+ class ConfigurationError < ArgumentError; end
10
+
11
+ DEFAULT_CONFIG = {
12
+ queue: 'sequel',
13
+ sequel: 'sqlite:/',
14
+ job_timeout: 5 # in seconds
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ def config
20
+ @config ||= OpenStruct.new DEFAULT_CONFIG
21
+ yield @config if block_given?
22
+ @config
23
+ end
24
+
25
+ # this boots queuing and working
26
+ # optional: to use another queuing or working subsystem just do
27
+ # 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
32
+
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
41
+
42
+ def boot_working
43
+ EQ::Working.boot
44
+ end
45
+
46
+ def queue
47
+ EQ::Queueing.queue
48
+ end
49
+
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
61
+
62
+ def logger
63
+ Celluloid.logger
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe EQ::Job do |variable|
4
+ it 'dumps const and payload' do
5
+ payload = EQ::Job.dump([EQ, 'bar', 'baz'])
6
+ Marshal.load(payload).should == [EQ, 'bar', 'baz']
7
+ end
8
+
9
+ it 'loads const and payload' do
10
+ serialized_payload = Marshal.dump [EQ, 'foo', 'bar']
11
+ job = EQ::Job.load(1, serialized_payload)
12
+ job.unpack.should == [EQ, 'foo', 'bar']
13
+ end
14
+
15
+ it 'performs using const.perform(*payload)' do
16
+ class MyJob
17
+ def self.perform(*args)
18
+ {result: args}
19
+ end
20
+ end
21
+ my_job_args = [1,2,3]
22
+ serialized_payload = EQ::Job.dump(MyJob, *my_job_args)
23
+ job = EQ::Job.load(1, serialized_payload)
24
+ job.perform.should == {result: my_job_args}
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe EQ::Queueing::Backends::Sequel do
4
+ subject { EQ::Queueing::Backends::Sequel.new 'sqlite:/' }
5
+ it_behaves_like 'abstract queue'
6
+ it_behaves_like 'queue backend'
7
+
8
+ it 'handles ::Sequel::DatabaseError with retry' do
9
+ db_method = subject.instance_eval('method(:jobs)')
10
+ raised = false
11
+ subject.stub(:jobs).and_return do |arg|
12
+ if raised
13
+ db_method.call
14
+ else
15
+ raised = true
16
+ raise ::Sequel::DatabaseError, "failed"
17
+ end
18
+ end
19
+ subject.waiting_count.should == 0
20
+ subject.push "foo"
21
+ subject.waiting_count.should == 1
22
+ end
23
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
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
53
+ end
54
+ subject { EQ::Queueing::Queue.new(queue_backend) }
55
+ it_behaves_like 'abstract queue'
56
+
57
+ it 'serializes jobs' do
58
+ EQ::Job.should_receive(:dump).with(["foo"])
59
+ subject.push "foo"
60
+ end
61
+
62
+ it 'deserializes jobs' do
63
+ subject.push "foo"
64
+ EQ::Job.should_receive(:load).with(1, EQ::Job.dump(["foo"]))
65
+ subject.reserve
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe EQ do
4
+
5
+ end
@@ -0,0 +1,22 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'eq', 'boot', 'all')
8
+ Dir[File.join(File.dirname(__FILE__), '/support/**/*.rb')].each {|f| require f; puts f}
9
+
10
+ RSpec.configure do |config|
11
+ config.treat_symbols_as_metadata_keys_with_true_values = true
12
+ config.run_all_when_everything_filtered = true
13
+ config.filter_run :focus
14
+
15
+ # Run specs in random order to surface order dependencies. If you find an
16
+ # order dependency and want to debug it, you can fix the order by providing
17
+ # the seed, which is printed after each run.
18
+ # --seed 1234
19
+ config.order = 'random'
20
+ end
21
+
22
+ require "timecop"
@@ -0,0 +1,75 @@
1
+ shared_examples_for 'queue backend' do
2
+ it 'pushes and pops' do
3
+ subject.push "foo"
4
+ job_id, payload = *subject.reserve
5
+ job_id.should == 1
6
+ payload.should == "foo"
7
+ end
8
+ end
9
+
10
+ shared_examples_for 'abstract queue' do
11
+ it 'pushes jobs' do
12
+ subject.waiting_count.should == 0
13
+ subject.working_count.should == 0
14
+ subject.push("foo").should == 1 # job id
15
+ subject.waiting_count.should == 1
16
+ subject.working_count.should == 0
17
+ end
18
+
19
+ it 'reserves jobs' do
20
+ id = subject.push "foo"
21
+ subject.reserve
22
+ subject.waiting_count.should == 0
23
+ subject.working_count.should == 1
24
+ subject.pop id
25
+ subject.waiting_count.should == 0
26
+ subject.working_count.should == 0
27
+ end
28
+
29
+ it 'pops jobs' do
30
+ subject.pop(1).should be_false # no job
31
+ subject.push "foo"
32
+ subject.pop(1).should be_true # one job
33
+ subject.pop(1).should be_false # again no job"
34
+ end
35
+
36
+ it 'puts working job back on waiting when they timeout via #requeue_timed_out_jobs' do
37
+ # freeze time on start of 1986
38
+ Timecop.freeze(Time.new(1986)) do
39
+
40
+ # create a job
41
+ id = subject.push "foo"
42
+
43
+ # start working
44
+ data = subject.reserve
45
+
46
+ # no on working at the beginning
47
+ subject.waiting_count.should == 0
48
+ subject.working_count.should == 1
49
+
50
+ # no one will be re-enqueued
51
+ subject.requeue_timed_out_jobs.should == 0
52
+
53
+ # no on working after senseless re-enqueueing
54
+ subject.waiting_count.should == 0
55
+ subject.working_count.should == 1
56
+
57
+ end
58
+
59
+ # freeze the time to 10s in the future
60
+ Timecop.freeze(Time.new(1986, 01, 01, 00, 00, EQ.config.job_timeout)) do
61
+
62
+ # nothing happened yet...
63
+ subject.waiting_count.should == 0
64
+ subject.working_count.should == 1
65
+
66
+ # this time one will be re-enqueued
67
+ subject.requeue_timed_out_jobs.should == 1
68
+
69
+ # now the old job is available again
70
+ subject.waiting_count.should == 1
71
+ subject.working_count.should == 0
72
+
73
+ end
74
+ end
75
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jens Bissinger
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sqlite3
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
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'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sequel
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
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'
46
+ - !ruby/object:Gem::Dependency
47
+ name: celluloid
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '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: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: guard-rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
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: timecop
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
+ description: Embedded Queueing. Background processing within a single process using
143
+ multi-threading and a SQL database.
144
+ email:
145
+ - mail@jens-bissinger.de
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - .gitignore
151
+ - .rspec
152
+ - .travis.yml
153
+ - Gemfile
154
+ - Guardfile
155
+ - LICENSE
156
+ - README.md
157
+ - Rakefile
158
+ - benchmarks/queueing.rb
159
+ - benchmarks/working.rb
160
+ - eq.gemspec
161
+ - examples/queueing.rb
162
+ - examples/simple_usage.rb
163
+ - examples/working.rb
164
+ - lib/eq-queueing.rb
165
+ - lib/eq-queueing/backends.rb
166
+ - lib/eq-queueing/backends/sequel.rb
167
+ - lib/eq-queueing/queue.rb
168
+ - lib/eq-working.rb
169
+ - lib/eq-working/manager.rb
170
+ - lib/eq-working/system.rb
171
+ - lib/eq-working/worker.rb
172
+ - lib/eq.rb
173
+ - lib/eq/boot/all.rb
174
+ - lib/eq/boot/queueing.rb
175
+ - lib/eq/boot/working.rb
176
+ - lib/eq/job.rb
177
+ - lib/eq/logging.rb
178
+ - lib/eq/version.rb
179
+ - spec/lib/eq-queueing/backends/sequel_spec.rb
180
+ - spec/lib/eq-queueing/queue_spec.rb
181
+ - spec/lib/eq/job_spec.rb
182
+ - spec/lib/eq_spec.rb
183
+ - spec/spec_helper.rb
184
+ - spec/support/shared_examples_for_queue.rb
185
+ homepage: https://github.com/dpree/eq
186
+ licenses: []
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ none: false
193
+ requirements:
194
+ - - ! '>='
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ none: false
199
+ requirements:
200
+ - - ! '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 1.8.23
206
+ signing_key:
207
+ specification_version: 3
208
+ summary: Based on Celluloid (multi-threading) and Sequel (SQLite3, MySQL, PostgreSQL,
209
+ ...).
210
+ test_files:
211
+ - spec/lib/eq-queueing/backends/sequel_spec.rb
212
+ - spec/lib/eq-queueing/queue_spec.rb
213
+ - spec/lib/eq/job_spec.rb
214
+ - spec/lib/eq_spec.rb
215
+ - spec/spec_helper.rb
216
+ - spec/support/shared_examples_for_queue.rb
217
+ has_rdoc: