procrastinate 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +30 -31
- data/Rakefile +1 -1
- data/lib/procrastinate/implicit.rb +58 -0
- data/lib/procrastinate/process_manager.rb +12 -39
- data/lib/procrastinate/process_manager/child_process.rb +26 -0
- data/lib/procrastinate/process_manager/object_endpoint.rb +10 -0
- data/lib/procrastinate/proxy.rb +1 -1
- data/lib/procrastinate/scheduler.rb +29 -14
- data/lib/procrastinate/spawn_strategy.rb +1 -0
- data/lib/procrastinate/spawn_strategy/default.rb +31 -0
- data/lib/procrastinate/task/result.rb +2 -0
- data/lib/procrastinate/utils/one_time_flag_ruby18_shim.rb +1 -8
- metadata +7 -3
data/README
CHANGED
@@ -4,13 +4,13 @@ INTRO
|
|
4
4
|
to concentrate on what to run when, not orchestration of low level details.
|
5
5
|
|
6
6
|
This library will be ideal for quickly scheduling of a lot of long running
|
7
|
-
tasks. You can easily control how many processes are run at any time.
|
8
|
-
|
7
|
+
tasks. You can easily control how many processes are run at any time. Your
|
8
|
+
main thread can continue to do useful work until it accesses the results of
|
9
|
+
the computation, at which point it will wait for the processes to finish.
|
9
10
|
|
10
11
|
SYNOPSIS
|
11
12
|
|
12
|
-
require 'procrastinate'
|
13
|
-
include Procrastinate
|
13
|
+
require 'procrastinate/implicit'
|
14
14
|
|
15
15
|
class Worker
|
16
16
|
def do_work
|
@@ -20,43 +20,42 @@ SYNOPSIS
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
worker = scheduler.create_proxy(Worker.new)
|
23
|
+
worker = Procrastinate.proxy(Worker.new)
|
25
24
|
|
26
25
|
10.times do
|
27
26
|
worker.do_work
|
28
27
|
end
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
Procrastinate.join
|
30
|
+
|
32
31
|
The above example will output something like
|
33
32
|
|
34
|
-
> Starting work in process
|
35
|
-
> Starting work in process
|
36
|
-
> Starting work in process
|
37
|
-
> Starting work in process
|
38
|
-
> Starting work in process
|
39
|
-
|
40
|
-
< Work completed in process
|
41
|
-
< Work completed in process
|
42
|
-
< Work completed in process
|
43
|
-
< Work completed in process
|
44
|
-
|
45
|
-
|
46
|
-
> Starting work in process
|
47
|
-
> Starting work in process
|
48
|
-
> Starting work in process
|
49
|
-
|
50
|
-
< Work completed in process
|
51
|
-
< Work completed in process
|
52
|
-
< Work completed in process
|
53
|
-
< Work completed in process
|
33
|
+
> Starting work in process 56144
|
34
|
+
> Starting work in process 56145
|
35
|
+
> Starting work in process 56146
|
36
|
+
> Starting work in process 56147
|
37
|
+
> Starting work in process 56148
|
38
|
+
> Starting work in process 56149
|
39
|
+
< Work completed in process 56144
|
40
|
+
< Work completed in process 56145
|
41
|
+
< Work completed in process 56146
|
42
|
+
< Work completed in process 56147
|
43
|
+
< Work completed in process 56148
|
44
|
+
< Work completed in process 56149
|
45
|
+
> Starting work in process 56150
|
46
|
+
> Starting work in process 56151
|
47
|
+
> Starting work in process 56152
|
48
|
+
> Starting work in process 56153
|
49
|
+
< Work completed in process 56150
|
50
|
+
< Work completed in process 56151
|
51
|
+
< Work completed in process 56152
|
52
|
+
< Work completed in process 56153
|
53
|
+
|
54
|
+
(The output depends on the number of cores your machine has)
|
54
55
|
|
55
56
|
COMPATIBILITY
|
56
57
|
|
57
|
-
This library runs with at least Ruby 1.8.7 and Ruby 1.9.
|
58
|
-
might be spotty, because the threading primitives in Ruby 1.9 are still
|
59
|
-
somewhat buggy.
|
58
|
+
This library runs with at least Ruby 1.8.7 and Ruby 1.9.
|
60
59
|
|
61
60
|
KNOWN BUGS
|
62
61
|
|
data/Rakefile
CHANGED
@@ -16,7 +16,7 @@ spec = Gem::Specification.new do |s|
|
|
16
16
|
|
17
17
|
# Change these as appropriate
|
18
18
|
s.name = "procrastinate"
|
19
|
-
s.version = "0.
|
19
|
+
s.version = "0.3.0"
|
20
20
|
s.summary = "Framework to run tasks in separate processes."
|
21
21
|
s.authors = ['Kaspar Schiess', 'Patrick Marchi']
|
22
22
|
s.email = ['kaspar.schiess@absurd.li', 'mail@patrickmarchi.ch']
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
require 'procrastinate'
|
3
|
+
|
4
|
+
module Procrastinate
|
5
|
+
# call-seq:
|
6
|
+
# Procrastinate.scheduler => scheduler
|
7
|
+
#
|
8
|
+
# Returns the scheduler instance. When using procrastinate/implicit, there
|
9
|
+
# is one global scheduler to your ruby process, this one.
|
10
|
+
#
|
11
|
+
def scheduler
|
12
|
+
@scheduler ||= Scheduler.start
|
13
|
+
end
|
14
|
+
module_function :scheduler
|
15
|
+
|
16
|
+
# call-seq:
|
17
|
+
# Procrastinate.proxy(obj) => proxy
|
18
|
+
#
|
19
|
+
# Creates a proxy that will execute methods called on obj in a child process.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# proxy = Procrastinate.proxy("foo")
|
24
|
+
# r = proxy += "bar"
|
25
|
+
# r.value # => 'foobar'
|
26
|
+
#
|
27
|
+
def proxy(obj)
|
28
|
+
scheduler.proxy(obj)
|
29
|
+
end
|
30
|
+
module_function :proxy
|
31
|
+
|
32
|
+
# call-seq:
|
33
|
+
# Procrastinate.join
|
34
|
+
#
|
35
|
+
# Waits for all currently scheduled tasks to complete. This is like calling
|
36
|
+
# #value on all result objects, except that nothing is returned.
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
#
|
40
|
+
# proxy = Procrastinate.proxy("foo")
|
41
|
+
# r = proxy += "bar"
|
42
|
+
# Procrastinate.join
|
43
|
+
# r.ready? # => true
|
44
|
+
#
|
45
|
+
def join
|
46
|
+
scheduler.join
|
47
|
+
end
|
48
|
+
module_function :join
|
49
|
+
|
50
|
+
# Internal method: You should not have to shutdown the scheduler manually
|
51
|
+
# since it consumes almost no resources when not active. This is mainly
|
52
|
+
# useful in tests to achieve isolation.
|
53
|
+
#
|
54
|
+
def shutdown
|
55
|
+
scheduler.shutdown
|
56
|
+
end
|
57
|
+
module_function :shutdown
|
58
|
+
end
|
@@ -8,6 +8,9 @@ require 'state_machine'
|
|
8
8
|
class Procrastinate::ProcessManager
|
9
9
|
include Procrastinate::IPC
|
10
10
|
|
11
|
+
autoload :ObjectEndpoint, 'procrastinate/process_manager/object_endpoint'
|
12
|
+
autoload :ChildProcess, 'procrastinate/process_manager/child_process'
|
13
|
+
|
11
14
|
# This pipe is used to wait for events in the master process.
|
12
15
|
attr_reader :control_pipe
|
13
16
|
|
@@ -15,45 +18,7 @@ class Procrastinate::ProcessManager
|
|
15
18
|
# processes we spawn. Once the process is complete, the callback is called
|
16
19
|
# in the procrastinate thread.
|
17
20
|
attr_reader :children
|
18
|
-
|
19
|
-
# A class that acts as a filter between ProcessManager and the endpoint it
|
20
|
-
# uses to communicate with its children. This converts Ruby objects into
|
21
|
-
# Strings and also sends process id.
|
22
|
-
#
|
23
|
-
class ObjectEndpoint < Struct.new(:endpoint, :pid)
|
24
|
-
def send(obj)
|
25
|
-
msg = Marshal.dump([pid, obj])
|
26
|
-
endpoint.send(msg)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# A <completion handler, result> tuple that stores the handler to call when
|
31
|
-
# a child exits and the object that will handle child-master communication
|
32
|
-
# if desired.
|
33
|
-
#
|
34
|
-
class Child < Struct.new(:handler, :result, :state)
|
35
|
-
state_machine :state, :initial => :new do
|
36
|
-
event(:start) { transition :new => :running }
|
37
|
-
event(:died) { transition :running => :dead }
|
38
21
|
|
39
|
-
after_transition :on => :died, :do => :call_completion_handlers
|
40
|
-
end
|
41
|
-
|
42
|
-
# Calls the completion handler for the child. This is triggered by the
|
43
|
-
# transition into the 'dead' state.
|
44
|
-
#
|
45
|
-
def call_completion_handlers
|
46
|
-
result.process_died if result
|
47
|
-
handler.call if handler
|
48
|
-
end
|
49
|
-
|
50
|
-
# Handles incoming messages from the tasks process.
|
51
|
-
#
|
52
|
-
def incoming_message(obj)
|
53
|
-
result.incoming_message(obj) if result
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
22
|
def initialize
|
58
23
|
# This controls process manager wakeup
|
59
24
|
@control_pipe = IO.pipe
|
@@ -101,6 +66,14 @@ class Procrastinate::ProcessManager
|
|
101
66
|
# Ignore:
|
102
67
|
end
|
103
68
|
|
69
|
+
# Returns the number of child processes that are alive at this point. Note
|
70
|
+
# that even if a child process is marked dead internally, it counts towards
|
71
|
+
# this number, since its results may not have been dispatched yet.
|
72
|
+
#
|
73
|
+
def process_count
|
74
|
+
children.count
|
75
|
+
end
|
76
|
+
|
104
77
|
# Internal methods below this point. ---------------------------------------
|
105
78
|
|
106
79
|
# Register signals that aid in child care. NB: Because we do this globally,
|
@@ -221,7 +194,7 @@ class Procrastinate::ProcessManager
|
|
221
194
|
# The spawning is done in the same thread as the reaping is done. This is
|
222
195
|
# why no race condition to the following line exists. (or in other code,
|
223
196
|
# for that matter.)
|
224
|
-
children[pid] =
|
197
|
+
children[pid] = ChildProcess.new(completion_handler, result).tap { |s| s.start }
|
225
198
|
end
|
226
199
|
|
227
200
|
# Gets executed in child process to clean up file handles and pipes that the
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# A <completion handler, result> tuple that stores the handler to call when
|
2
|
+
# a child exits and the object that will handle child-master communication
|
3
|
+
# if desired.
|
4
|
+
#
|
5
|
+
class Procrastinate::ProcessManager::ChildProcess < Struct.new(:handler, :result, :state)
|
6
|
+
state_machine :state, :initial => :new do
|
7
|
+
event(:start) { transition :new => :running }
|
8
|
+
event(:died) { transition :running => :dead }
|
9
|
+
|
10
|
+
after_transition :on => :died, :do => :call_completion_handlers
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calls the completion handler for the child. This is triggered by the
|
14
|
+
# transition into the 'dead' state.
|
15
|
+
#
|
16
|
+
def call_completion_handlers
|
17
|
+
result.process_died if result
|
18
|
+
handler.call if handler
|
19
|
+
end
|
20
|
+
|
21
|
+
# Handles incoming messages from the tasks process.
|
22
|
+
#
|
23
|
+
def incoming_message(obj)
|
24
|
+
result.incoming_message(obj) if result
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# A class that acts as a filter between ProcessManager and the endpoint it
|
2
|
+
# uses to communicate with its children. This converts Ruby objects into
|
3
|
+
# Strings and also sends process id.
|
4
|
+
#
|
5
|
+
class Procrastinate::ProcessManager::ObjectEndpoint < Struct.new(:endpoint, :pid)
|
6
|
+
def send(obj)
|
7
|
+
msg = Marshal.dump([pid, obj])
|
8
|
+
endpoint.send(msg)
|
9
|
+
end
|
10
|
+
end
|
data/lib/procrastinate/proxy.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
class Procrastinate::Proxy
|
5
5
|
# Create a new proxy class. +worker+ is an instance of the class that we
|
6
6
|
# want to perform work in, +scheduler+ is where the work will be scheduled.
|
7
|
-
# Don't call this on your own, instead use Scheduler#
|
7
|
+
# Don't call this on your own, instead use Scheduler#proxy.
|
8
8
|
#
|
9
9
|
def initialize(worker, scheduler) # :nodoc:
|
10
10
|
@worker = worker
|
@@ -14,7 +14,7 @@ class Procrastinate::Scheduler
|
|
14
14
|
attr_reader :task_queue
|
15
15
|
|
16
16
|
def initialize(strategy)
|
17
|
-
@strategy = strategy || Procrastinate::SpawnStrategy::
|
17
|
+
@strategy = strategy || Procrastinate::SpawnStrategy::Default.new
|
18
18
|
@manager = Procrastinate::ProcessManager.new
|
19
19
|
|
20
20
|
# State takes three values: :running, :soft_shutdown, :real_shutdown
|
@@ -39,10 +39,10 @@ class Procrastinate::Scheduler
|
|
39
39
|
#
|
40
40
|
# Example:
|
41
41
|
#
|
42
|
-
# proxy = scheduler.
|
42
|
+
# proxy = scheduler.proxy(worker)
|
43
43
|
# status = proxy.do_some_work # will execute later and in its own process
|
44
44
|
#
|
45
|
-
def
|
45
|
+
def proxy(worker)
|
46
46
|
return Procrastinate::Proxy.new(worker, self)
|
47
47
|
end
|
48
48
|
|
@@ -65,17 +65,31 @@ class Procrastinate::Scheduler
|
|
65
65
|
manager.wakeup
|
66
66
|
end
|
67
67
|
|
68
|
+
# Waits for the currently queued work to complete. This can be used at the
|
69
|
+
# end of short scripts to ensure that all work is done.
|
70
|
+
#
|
71
|
+
def join
|
72
|
+
@state = :soft_shutdown
|
73
|
+
|
74
|
+
# NOTE: Currently, this method busy-loops until all childs terminate.
|
75
|
+
# This is not as elegant as I whish it to be, but its a start.
|
76
|
+
|
77
|
+
# Wait until all tasks are done.
|
78
|
+
loop do
|
79
|
+
manager.wakeup
|
80
|
+
break if task_queue.empty? && manager.process_count==0
|
81
|
+
sleep 0.01
|
82
|
+
end
|
83
|
+
|
84
|
+
ensure
|
85
|
+
@state = :running
|
86
|
+
end
|
87
|
+
|
68
88
|
# Immediately shuts down the procrastinate thread and frees resources.
|
69
89
|
# If there are any tasks left in the queue, they will NOT be executed.
|
70
90
|
#
|
71
91
|
def shutdown(hard=false)
|
72
|
-
unless hard
|
73
|
-
@state = :soft_shutdown
|
74
|
-
loop do
|
75
|
-
manager.wakeup
|
76
|
-
break if task_queue.empty?
|
77
|
-
end
|
78
|
-
end
|
92
|
+
join unless hard
|
79
93
|
|
80
94
|
# Set the flag that will provoke shutdown
|
81
95
|
@state = :real_shutdown
|
@@ -87,21 +101,22 @@ class Procrastinate::Scheduler
|
|
87
101
|
end
|
88
102
|
|
89
103
|
private
|
90
|
-
# Spawns new tasks (if needed).
|
91
|
-
# thread
|
92
|
-
#
|
104
|
+
# Spawns new tasks (if needed).
|
105
|
+
# *control thread*
|
106
|
+
#
|
93
107
|
def spawn
|
94
108
|
while strategy.should_spawn? && !task_queue.empty?
|
95
109
|
task = task_queue.pop
|
110
|
+
strategy.notify_spawn
|
96
111
|
manager.create_process(task) do
|
97
112
|
strategy.notify_dead
|
98
113
|
end
|
99
|
-
strategy.notify_spawn
|
100
114
|
end
|
101
115
|
end
|
102
116
|
|
103
117
|
# This is the content of the control thread that is spawned with
|
104
118
|
# #start_thread
|
119
|
+
# *control thread*
|
105
120
|
#
|
106
121
|
def run
|
107
122
|
# Start managers work
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
# A dispatcher strategy that throttles tasks starting and ensures that no more
|
3
|
+
# than limit processes run concurrently. Limit is initialized to the number of
|
4
|
+
# cores this system has (+1) or a default value of 5 processes if the number
|
5
|
+
# of cores cannot be autodetected.
|
6
|
+
#
|
7
|
+
class Procrastinate::SpawnStrategy::Default < Procrastinate::SpawnStrategy::Throttled
|
8
|
+
def initialize(workload_factor=3)
|
9
|
+
# In reality, this depends on what workload you have. You might want to
|
10
|
+
# tune this number.
|
11
|
+
super(autodetect_cores*workload_factor)
|
12
|
+
end
|
13
|
+
|
14
|
+
def autodetect_cores
|
15
|
+
# Linux / all OS with a /proc filesystem
|
16
|
+
if File.exist?('/proc/cpuinfo')
|
17
|
+
return Integer(`cat /proc/cpuinfo | grep "processor" | wc -l`.chomp)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Mac OS X
|
21
|
+
if File.exist?('/usr/sbin/system_profiler')
|
22
|
+
output = `system_profiler SPHardwareDataType | grep "Total Number Of Cores"`
|
23
|
+
if md=output.match(%r(Total Number Of Cores: (\d+)))
|
24
|
+
return Integer(md[1])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
warn "Could not detect the number of CPU cores. Using a default of 2."
|
29
|
+
return 2
|
30
|
+
end
|
31
|
+
end
|
@@ -12,6 +12,7 @@ class Procrastinate::Task::Result
|
|
12
12
|
end
|
13
13
|
|
14
14
|
# Gets passed all messages sent by the child process for this task.
|
15
|
+
# *control thread*
|
15
16
|
#
|
16
17
|
def incoming_message(obj)
|
17
18
|
return if ready?
|
@@ -23,6 +24,7 @@ class Procrastinate::Task::Result
|
|
23
24
|
# Notifies this result that the process has died. If this happens before
|
24
25
|
# a process result is passed to #incoming_message, that message will never
|
25
26
|
# arrive.
|
27
|
+
# *control thread*
|
26
28
|
#
|
27
29
|
def process_died
|
28
30
|
return if ready?
|
@@ -1,7 +1,5 @@
|
|
1
1
|
class Procrastinate::Utils::OneTimeFlag
|
2
2
|
def initialize
|
3
|
-
@waiting_m = Mutex.new
|
4
|
-
@waiting_cv = ConditionVariable.new
|
5
3
|
@set = false
|
6
4
|
end
|
7
5
|
|
@@ -10,18 +8,13 @@ class Procrastinate::Utils::OneTimeFlag
|
|
10
8
|
def wait
|
11
9
|
return if set?
|
12
10
|
|
13
|
-
|
14
|
-
@waiting_cv.wait(@waiting_m)
|
15
|
-
end
|
11
|
+
sleep(0.01) until set?
|
16
12
|
end
|
17
13
|
|
18
14
|
# Sets the flag and releases all waiting threads.
|
19
15
|
#
|
20
16
|
def set
|
21
17
|
@set = true
|
22
|
-
@waiting_m.synchronize do
|
23
|
-
@waiting_cv.broadcast
|
24
|
-
end
|
25
18
|
end
|
26
19
|
|
27
20
|
# Non blocking: Is the flag set?
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.3.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Kaspar Schiess
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-12-
|
18
|
+
date: 2010-12-27 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -73,13 +73,17 @@ files:
|
|
73
73
|
- LICENSE
|
74
74
|
- Rakefile
|
75
75
|
- README
|
76
|
+
- lib/procrastinate/implicit.rb
|
76
77
|
- lib/procrastinate/ipc/endpoint.rb
|
77
78
|
- lib/procrastinate/ipc.rb
|
78
79
|
- lib/procrastinate/lock.rb
|
80
|
+
- lib/procrastinate/process_manager/child_process.rb
|
81
|
+
- lib/procrastinate/process_manager/object_endpoint.rb
|
79
82
|
- lib/procrastinate/process_manager.rb
|
80
83
|
- lib/procrastinate/proxy.rb
|
81
84
|
- lib/procrastinate/runtime.rb
|
82
85
|
- lib/procrastinate/scheduler.rb
|
86
|
+
- lib/procrastinate/spawn_strategy/default.rb
|
83
87
|
- lib/procrastinate/spawn_strategy/simple.rb
|
84
88
|
- lib/procrastinate/spawn_strategy/throttled.rb
|
85
89
|
- lib/procrastinate/spawn_strategy.rb
|