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 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