procrastinate 0.2.0 → 0.3.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.
- 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
|