tennis-jobs 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.markdown +38 -169
- data/examples/async_sum.rb +36 -0
- data/lib/tennis.rb +6 -11
- data/lib/tennis/action.rb +30 -0
- data/lib/tennis/actor.rb +49 -0
- data/lib/tennis/backend/abstract.rb +38 -0
- data/lib/tennis/backend/memory.rb +57 -0
- data/lib/tennis/{serializer/generic.rb → backend/serializer.rb} +30 -13
- data/lib/tennis/backend/task.rb +36 -0
- data/lib/tennis/cli.rb +21 -61
- data/lib/tennis/configuration.rb +9 -10
- data/lib/tennis/exceptions.rb +3 -0
- data/lib/tennis/fetcher.rb +36 -0
- data/lib/tennis/job.rb +30 -0
- data/lib/tennis/launcher.rb +38 -0
- data/lib/tennis/worker.rb +46 -0
- data/lib/tennis/worker_pool.rb +114 -0
- data/tennis-jobs.gemspec +4 -4
- metadata +19 -16
- data/examples/deferable.rb +0 -46
- data/examples/example.rb +0 -52
- data/examples/serializer.rb +0 -37
- data/lib/tennis/worker/deferable.rb +0 -55
- data/lib/tennis/worker/deferable/action.rb +0 -25
- data/lib/tennis/worker/generic.rb +0 -58
- data/lib/tennis/worker/generic/before_hook.rb +0 -17
- data/lib/tennis/worker/generic/options.rb +0 -19
- data/lib/tennis/worker/generic/serialize.rb +0 -26
@@ -1,12 +1,13 @@
|
|
1
1
|
require "json"
|
2
2
|
|
3
3
|
module Tennis
|
4
|
-
module
|
5
|
-
class
|
4
|
+
module Backend
|
5
|
+
class Serializer
|
6
6
|
|
7
7
|
RECOGNIZED_TYPES = {
|
8
|
-
|
9
|
-
class:
|
8
|
+
findable: "findable".freeze,
|
9
|
+
class: "class".freeze,
|
10
|
+
job: "job".freeze,
|
10
11
|
}.freeze
|
11
12
|
|
12
13
|
def load(message)
|
@@ -30,12 +31,18 @@ module Tennis
|
|
30
31
|
_type: RECOGNIZED_TYPES[:class],
|
31
32
|
_class: object.to_s
|
32
33
|
}
|
33
|
-
when :
|
34
|
+
when :findable
|
34
35
|
{
|
35
|
-
_type: RECOGNIZED_TYPES[:
|
36
|
+
_type: RECOGNIZED_TYPES[:findable],
|
36
37
|
_class: object.class.to_s,
|
37
38
|
_id: object.id,
|
38
39
|
}
|
40
|
+
when :job
|
41
|
+
{
|
42
|
+
_type: RECOGNIZED_TYPES[:job],
|
43
|
+
_class: object.class.to_s,
|
44
|
+
_dump: object.job_dump,
|
45
|
+
}
|
39
46
|
else
|
40
47
|
fail "Unexpected type: #{type} when visiting object"
|
41
48
|
end
|
@@ -49,9 +56,12 @@ module Tennis
|
|
49
56
|
object
|
50
57
|
when :class
|
51
58
|
Object.const_get(object["_class"])
|
52
|
-
when :
|
59
|
+
when :findable
|
53
60
|
klass = Object.const_get(object["_class"])
|
54
61
|
klass.find(object["_id"])
|
62
|
+
when :job
|
63
|
+
klass = Object.const_get(object["_class"])
|
64
|
+
klass.job_load(object["_dump"])
|
55
65
|
else
|
56
66
|
fail "Unexpected type: #{type} when visiting object"
|
57
67
|
end
|
@@ -65,8 +75,10 @@ module Tennis
|
|
65
75
|
def visit_any(object, block)
|
66
76
|
if object.kind_of?(Array)
|
67
77
|
visit_array(object, block)
|
68
|
-
elsif
|
69
|
-
block.call(:
|
78
|
+
elsif is_job?(object)
|
79
|
+
block.call(:job, object)
|
80
|
+
elsif is_findable?(object)
|
81
|
+
block.call(:findable, object)
|
70
82
|
elsif is_class?(object)
|
71
83
|
block.call(:class, object)
|
72
84
|
elsif object.kind_of?(Hash)
|
@@ -86,14 +98,19 @@ module Tennis
|
|
86
98
|
end
|
87
99
|
end
|
88
100
|
|
89
|
-
def
|
90
|
-
object.respond_to?(:
|
91
|
-
object.is_a?(Hash) && object["_type"] == RECOGNIZED_TYPES[:
|
101
|
+
def is_findable?(object)
|
102
|
+
(object.class.respond_to?(:find) && object.respond_to?(:id)) ||
|
103
|
+
(object.is_a?(Hash) && object["_type"] == RECOGNIZED_TYPES[:findable])
|
92
104
|
end
|
93
105
|
|
94
106
|
def is_class?(object)
|
95
107
|
object.is_a?(Class) ||
|
96
|
-
object.is_a?(Hash) && object["_type"] == RECOGNIZED_TYPES[:class]
|
108
|
+
(object.is_a?(Hash) && object["_type"] == RECOGNIZED_TYPES[:class])
|
109
|
+
end
|
110
|
+
|
111
|
+
def is_job?(object)
|
112
|
+
object.is_a?(Tennis::Job) ||
|
113
|
+
(object.is_a?(Hash) && object["_type"] == RECOGNIZED_TYPES[:job])
|
97
114
|
end
|
98
115
|
|
99
116
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Tennis
|
2
|
+
module Backend
|
3
|
+
class Task
|
4
|
+
|
5
|
+
attr_reader :task_id, :job, :method, :args, :meta
|
6
|
+
attr_accessor :worker
|
7
|
+
|
8
|
+
def initialize(backend, task_id, job, method, args, meta = {})
|
9
|
+
@backend, @task_id, @acked = backend, task_id, false
|
10
|
+
@job, @method, @args = job, method, args
|
11
|
+
@meta = meta
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
@job.__send__(@method, *@args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ack
|
19
|
+
return if acked?
|
20
|
+
@backend.ack(self)
|
21
|
+
@acked = true
|
22
|
+
end
|
23
|
+
|
24
|
+
def requeue
|
25
|
+
return if acked?
|
26
|
+
@backend.requeue(self)
|
27
|
+
@acked = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def acked?
|
31
|
+
@acked
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/tennis/cli.rb
CHANGED
@@ -1,32 +1,30 @@
|
|
1
|
-
require "yaml"
|
2
1
|
require "optparse"
|
3
|
-
|
4
|
-
require "
|
2
|
+
|
3
|
+
require "tennis/launcher"
|
5
4
|
|
6
5
|
module Tennis
|
7
6
|
class CLI
|
8
7
|
|
9
8
|
DEFAULT_OPTIONS = {
|
10
|
-
|
9
|
+
concurrency: 2,
|
10
|
+
job_class_names: [],
|
11
11
|
}.freeze
|
12
12
|
|
13
13
|
def self.start
|
14
14
|
options = DEFAULT_OPTIONS.dup
|
15
15
|
OptionParser.new do |opts|
|
16
|
-
opts.banner = "Usage: tennis [options]
|
17
|
-
opts.on("-
|
18
|
-
options[:
|
16
|
+
opts.banner = "Usage: tennis [options]"
|
17
|
+
opts.on("-j", "--jobs JOBS", "List of the job classes to handle") do |jobs|
|
18
|
+
options[:job_class_names] = jobs.split(",")
|
19
|
+
end
|
20
|
+
opts.on("-c", "--concurrency COUNT", "The number of concurrent jobs") do |concurrency|
|
21
|
+
options[:concurrency] = concurrency.to_i
|
19
22
|
end
|
20
23
|
opts.on("-r", "--require PATH", "Require files before starting") do |path|
|
21
24
|
options[:require] ||= []
|
22
25
|
options[:require] << path
|
23
26
|
end
|
24
|
-
opts.on("-x", "--execute CODE", "Execute code before starting") do |code|
|
25
|
-
options[:execute] ||= []
|
26
|
-
options[:execute] << code
|
27
|
-
end
|
28
27
|
end.parse!
|
29
|
-
options[:group] = ARGV.first
|
30
28
|
new(options).start
|
31
29
|
end
|
32
30
|
|
@@ -35,68 +33,30 @@ module Tennis
|
|
35
33
|
end
|
36
34
|
|
37
35
|
def start
|
38
|
-
|
39
|
-
|
40
|
-
configure_tennis
|
41
|
-
start_group
|
36
|
+
require_paths
|
37
|
+
start_launcher
|
42
38
|
end
|
43
39
|
|
44
40
|
private
|
45
41
|
|
46
|
-
def
|
42
|
+
def require_paths
|
47
43
|
return unless requires = @options[:require]
|
48
44
|
requires.each { |path| require path } if @options[:require]
|
49
45
|
end
|
50
46
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
Tennis.configure do |config|
|
58
|
-
config.async = true
|
59
|
-
config.exchange = group["exchange"]
|
60
|
-
config.workers = group["workers"].to_i
|
61
|
-
config.logger = Logger.new(STDOUT)
|
62
|
-
config.logger.level = Logger::WARN
|
63
|
-
config.sneakers_options = sneakers_options
|
64
|
-
end
|
47
|
+
def start_launcher
|
48
|
+
raise "You must specify at least one job class" if job_classes.empty?
|
49
|
+
Launcher.new({
|
50
|
+
job_classes: job_classes,
|
51
|
+
concurrency: @options[:concurrency]
|
52
|
+
}).start
|
65
53
|
end
|
66
54
|
|
67
|
-
def
|
68
|
-
|
69
|
-
merge_options(all_options, options)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def merge_options(target, options)
|
74
|
-
options.each do |name, value|
|
75
|
-
if target[name].nil?
|
76
|
-
target[name] = value
|
77
|
-
elsif target[name] != value
|
78
|
-
fail "Workers shouldn't have different '#{name}' options"
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def start_group
|
84
|
-
Sneakers::Runner.new(classes.map(&:worker)).run
|
85
|
-
end
|
86
|
-
|
87
|
-
def classes
|
88
|
-
@classes ||= group["classes"].map do |name|
|
55
|
+
def job_classes
|
56
|
+
@job_classes ||= @options[:job_class_names].map do |name|
|
89
57
|
Object.const_get(name)
|
90
58
|
end
|
91
59
|
end
|
92
60
|
|
93
|
-
def group
|
94
|
-
@group ||= config[@options[:group]]
|
95
|
-
end
|
96
|
-
|
97
|
-
def config
|
98
|
-
YAML.load_file(@options[:config])
|
99
|
-
end
|
100
|
-
|
101
61
|
end
|
102
62
|
end
|
data/lib/tennis/configuration.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "celluloid"
|
3
|
+
|
1
4
|
module Tennis
|
2
5
|
class Configuration
|
3
6
|
DEFAULT = {
|
4
7
|
async: true,
|
5
|
-
|
6
|
-
workers: 4,
|
7
|
-
logger: STDOUT,
|
8
|
-
sneakers_options: {},
|
8
|
+
logger: Logger.new(STDOUT),
|
9
9
|
}.freeze
|
10
10
|
|
11
|
-
attr_accessor :async, :
|
11
|
+
attr_accessor :async, :logger, :backend
|
12
12
|
|
13
13
|
def initialize(opts = {})
|
14
14
|
DEFAULT.merge(opts).each do |name, value|
|
@@ -17,11 +17,10 @@ module Tennis
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def finalize!
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
}.merge(sneakers_options))
|
20
|
+
raise "You must specify a backend during the configuration" unless backend
|
21
|
+
|
22
|
+
# Set the celluloid logger.
|
23
|
+
Celluloid.logger = logger
|
25
24
|
end
|
26
25
|
|
27
26
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "tennis/actor"
|
2
|
+
|
3
|
+
module Tennis
|
4
|
+
class Fetcher
|
5
|
+
include Actor
|
6
|
+
|
7
|
+
attr_reader :worker_pool
|
8
|
+
|
9
|
+
def initialize(worker_pool, options)
|
10
|
+
@job_classes = options[:job_classes]
|
11
|
+
@worker_pool = worker_pool
|
12
|
+
@backend = Tennis.config.backend
|
13
|
+
@done = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch
|
17
|
+
return if done?
|
18
|
+
if task = @backend.receive(job_classes: @job_classes)
|
19
|
+
worker_pool.async.work(task)
|
20
|
+
else
|
21
|
+
async.fetch
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def done!
|
26
|
+
@done = true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def done?
|
32
|
+
@done
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/tennis/job.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "tennis/action"
|
2
|
+
require "tennis/worker"
|
3
|
+
|
4
|
+
module Tennis
|
5
|
+
module Job
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return a proxy object that will enqueue method calls into
|
11
|
+
# the Tennis's backend.
|
12
|
+
def async
|
13
|
+
Action.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Dump a Job instance into a simple hash.
|
17
|
+
def job_dump
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# Build a Job instance from a simple hash.
|
24
|
+
def job_load(hash)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "celluloid/condition"
|
2
|
+
|
3
|
+
require "tennis/actor"
|
4
|
+
require "tennis/fetcher"
|
5
|
+
require "tennis/worker_pool"
|
6
|
+
|
7
|
+
module Tennis
|
8
|
+
class Launcher
|
9
|
+
include Actor
|
10
|
+
|
11
|
+
attr_reader :worker_pool, :fetcher
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@stop_condition = Celluloid::Condition.new
|
15
|
+
@worker_pool = WorkerPool.new_link(@stop_condition, options)
|
16
|
+
@fetcher = Fetcher.new_link(worker_pool, options)
|
17
|
+
@worker_pool.fetcher = @fetcher
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
worker_pool.async.start
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop
|
25
|
+
# Stop fetching
|
26
|
+
fetcher.done!
|
27
|
+
|
28
|
+
# Gracefully stop the workers that are still working
|
29
|
+
worker_pool.async.stop
|
30
|
+
@stop_condition.wait
|
31
|
+
|
32
|
+
# Terminate the two actors
|
33
|
+
worker_pool.terminate if worker_pool.alive?
|
34
|
+
fetcher.terminate if fetcher.alive?
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "tennis/actor"
|
2
|
+
require "tennis/exceptions"
|
3
|
+
|
4
|
+
module Tennis
|
5
|
+
class Worker
|
6
|
+
include Actor
|
7
|
+
|
8
|
+
attr_accessor :worker_id
|
9
|
+
|
10
|
+
def initialize(pool)
|
11
|
+
@pool = pool
|
12
|
+
end
|
13
|
+
|
14
|
+
def work(task)
|
15
|
+
# Send the current working thread to the pool.
|
16
|
+
register_working_thread
|
17
|
+
|
18
|
+
ack = true
|
19
|
+
begin
|
20
|
+
task.execute
|
21
|
+
rescue Shutdown
|
22
|
+
ack = false
|
23
|
+
raise
|
24
|
+
rescue Exception => exception
|
25
|
+
# TODO: add an error handler on the job's class
|
26
|
+
raise
|
27
|
+
ensure
|
28
|
+
task.ack if ack
|
29
|
+
end
|
30
|
+
|
31
|
+
# Tell the pool that we've successfully done the job.
|
32
|
+
notifies_work_done(task)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def register_working_thread
|
38
|
+
@pool.async.register_thread(worker_id, Thread.current)
|
39
|
+
end
|
40
|
+
|
41
|
+
def notifies_work_done(task)
|
42
|
+
@pool.async.work_done(task)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
require "tennis/actor"
|
4
|
+
require "tennis/exceptions"
|
5
|
+
require "tennis/worker"
|
6
|
+
|
7
|
+
module Tennis
|
8
|
+
class WorkerPool
|
9
|
+
include Actor
|
10
|
+
|
11
|
+
trap_exit :worker_died
|
12
|
+
|
13
|
+
attr_accessor :fetcher
|
14
|
+
|
15
|
+
def initialize(stop_condition, options)
|
16
|
+
@stop_condition = stop_condition
|
17
|
+
@size = options[:concurrency]
|
18
|
+
@pending_tasks = []
|
19
|
+
@threads = {}
|
20
|
+
@workers = Queue.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
@size.times { start_worker }
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop(timeout: 30)
|
28
|
+
done!
|
29
|
+
|
30
|
+
if @pending_tasks.empty?
|
31
|
+
shutdown
|
32
|
+
elsif timeout
|
33
|
+
plan_hard_shutdown timeout
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def work(task)
|
38
|
+
# Do not accept new tasks if done.
|
39
|
+
return task.requeue if done?
|
40
|
+
|
41
|
+
@pending_tasks << task
|
42
|
+
worker = @workers.pop(true)
|
43
|
+
task.worker = worker
|
44
|
+
worker.async.work(task)
|
45
|
+
end
|
46
|
+
|
47
|
+
def work_done(task)
|
48
|
+
@pending_tasks.delete(task)
|
49
|
+
@threads.delete(task.worker.object_id)
|
50
|
+
ready(task.worker) if task.worker.alive?
|
51
|
+
|
52
|
+
# If done and there is no more pending tasks, we can shutdown. It also
|
53
|
+
# means that every workers are in que @workers queue.
|
54
|
+
shutdown if done? && @pending_tasks.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
def register_thread(worker_id, thread)
|
58
|
+
@threads[worker_id] = thread
|
59
|
+
end
|
60
|
+
|
61
|
+
def worker_died(worker, reason)
|
62
|
+
@threads.delete(worker.object_id)
|
63
|
+
@pending_tasks.delete_if { |task| task.worker == worker }
|
64
|
+
start_worker unless reason.is_a?(Shutdown)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def done!
|
70
|
+
@done = true
|
71
|
+
end
|
72
|
+
|
73
|
+
def done?
|
74
|
+
@done
|
75
|
+
end
|
76
|
+
|
77
|
+
def plan_hard_shutdown(timeout)
|
78
|
+
after(timeout) do
|
79
|
+
@pending_tasks.each do |task|
|
80
|
+
worker = task.worker
|
81
|
+
thread = @threads.delete(worker.object_id)
|
82
|
+
thread.raise(Shutdown) if worker.alive?
|
83
|
+
task.requeue
|
84
|
+
end
|
85
|
+
@pending_tasks.clear
|
86
|
+
|
87
|
+
shutdown
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def shutdown
|
92
|
+
# Terminate the worker actors.
|
93
|
+
@workers.size.times do
|
94
|
+
worker = @workers.pop
|
95
|
+
worker.terminate if worker.alive?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Signal the launcher that we're done processing jobs
|
99
|
+
@stop_condition.signal
|
100
|
+
end
|
101
|
+
|
102
|
+
def start_worker
|
103
|
+
worker = Worker.new_link(current_actor)
|
104
|
+
worker.worker_id = worker.object_id
|
105
|
+
ready(worker)
|
106
|
+
end
|
107
|
+
|
108
|
+
def ready(worker)
|
109
|
+
@workers << worker
|
110
|
+
fetcher.async.fetch
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|