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 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. To get
8
- at/cron like scheduling, combine this library with 'rufus-scheduler'.
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
- scheduler = Scheduler.start(SpawnStrategy::Throttled.new(5))
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
- scheduler.shutdown
31
-
29
+ Procrastinate.join
30
+
32
31
  The above example will output something like
33
32
 
34
- > Starting work in process 6651
35
- > Starting work in process 6652
36
- > Starting work in process 6653
37
- > Starting work in process 6654
38
- > Starting work in process 6655
39
- < Work completed in process 6651
40
- < Work completed in process 6652
41
- < Work completed in process 6653
42
- < Work completed in process 6654
43
- < Work completed in process 6655
44
- > Starting work in process 6659
45
- > Starting work in process 6660
46
- > Starting work in process 6656
47
- > Starting work in process 6657
48
- > Starting work in process 6658
49
- < Work completed in process 6659
50
- < Work completed in process 6660
51
- < Work completed in process 6656
52
- < Work completed in process 6657
53
- < Work completed in process 6658
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. Ruby 1.9 support
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.2.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] = Child.new(completion_handler, result).tap { |s| s.start }
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
@@ -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#create_proxy.
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::Simple.new
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.create_proxy(worker)
42
+ # proxy = scheduler.proxy(worker)
43
43
  # status = proxy.do_some_work # will execute later and in its own process
44
44
  #
45
- def create_proxy(worker)
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). This is only ever called from the control
91
- # thread (see below).
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
@@ -2,4 +2,5 @@
2
2
  module Procrastinate::SpawnStrategy
3
3
  autoload :Simple, 'procrastinate/spawn_strategy/simple'
4
4
  autoload :Throttled, 'procrastinate/spawn_strategy/throttled'
5
+ autoload :Default, 'procrastinate/spawn_strategy/default'
5
6
  end
@@ -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
- @waiting_m.synchronize do
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
- - 2
7
+ - 3
8
8
  - 0
9
- version: 0.2.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-22 00:00:00 +01:00
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