procrastinate 0.1.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2010 Kaspar Schiess, Patrick Marchi
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,76 @@
1
+ INTRO
2
+
3
+ 'procrastinate' does the process handling so you don't have to. It leaves you
4
+ to concentrate on what to run when, not orchestration of low level details.
5
+
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'.
9
+
10
+ SYNOPSIS
11
+
12
+ require 'procrastinate'
13
+ include Procrastinate
14
+
15
+ class Worker
16
+ def do_work
17
+ puts "> Starting work in process #{Process.pid}"
18
+ sleep 2
19
+ puts "< Work completed in process #{Process.pid}"
20
+ end
21
+ end
22
+
23
+ scheduler = Scheduler.start(DispatchStrategy::Throttled.new(5))
24
+ worker = scheduler.create_proxy(Worker.new)
25
+
26
+ 10.times do
27
+ worker.do_work
28
+ end
29
+
30
+ scheduler.shutdown
31
+
32
+ The above example will output something like
33
+
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
54
+
55
+ COMPATIBILITY
56
+
57
+ This library runs with at least Ruby 1.8.7 and Ruby 1.9. For running it with
58
+ Ruby 1.9, you need to patch it with the patch found at
59
+
60
+ http://redmine.ruby-lang.org/issues/show/1525
61
+
62
+ or run it with at least r25844 of Ruby trunk.
63
+
64
+ KNOWN BUGS
65
+
66
+ Due to the way we handle signal traps, you cannot start more than one
67
+ Scheduler. We might allow that in the future.
68
+
69
+ STATUS
70
+
71
+ This code is not released as a gem yet. Once we iron out the bugs we'll
72
+ release it as 0.1.0. Alpha quality code.
73
+
74
+ Please see the LICENSE file for license information.
75
+
76
+ (c) 2010 Kaspar Schiess, Patrick Marchi
data/Rakefile ADDED
@@ -0,0 +1,69 @@
1
+ require 'rspec'
2
+ require 'rspec/core/rake_task'
3
+ Rspec::Core::RakeTask.new
4
+ task :default => :spec
5
+
6
+ task :default => :spec
7
+
8
+ require "rubygems"
9
+ require "rake/gempackagetask"
10
+ require "rake/rdoctask"
11
+
12
+ # This builds the actual gem. For details of what all these options
13
+ # mean, and other ones you can add, check the documentation here:
14
+ #
15
+ # http://rubygems.org/read/chapter/20
16
+ #
17
+ spec = Gem::Specification.new do |s|
18
+
19
+ # Change these as appropriate
20
+ s.name = "procrastinate"
21
+ s.version = "0.1.0"
22
+ s.summary = "Framework to run tasks in separate processes."
23
+ s.authors = ['Kaspar Schiess', 'Patrick Marchi']
24
+ s.email = ['kaspar.schiess@absurd.li', 'mail@patrickmarchi.ch']
25
+ s.homepage = "http://github.com/kschiess/procrastinate"
26
+
27
+ s.has_rdoc = true
28
+ s.extra_rdoc_files = %w(README)
29
+ s.rdoc_options = %w(--main README)
30
+
31
+ # Add any extra files to include in the gem
32
+ s.files = %w(LICENSE Rakefile README) + Dir.glob("{spec,lib/**/*}")
33
+ s.require_paths = ["lib"]
34
+
35
+ # If you want to depend on other gems, add them here, along with any
36
+ # relevant versions
37
+ # s.add_dependency("blankslate", "~> 2.0")
38
+
39
+ # If your tests use any gems, include them here
40
+ s.add_development_dependency("rspec")
41
+ s.add_development_dependency("flexmock")
42
+ end
43
+
44
+ # This task actually builds the gem. We also regenerate a static
45
+ # .gemspec file, which is useful if something (i.e. GitHub) will
46
+ # be automatically building a gem for this project. If you're not
47
+ # using GitHub, edit as appropriate.
48
+ #
49
+ # To publish your gem online, install the 'gemcutter' gem; Read more
50
+ # about that here: http://gemcutter.org/pages/gem_docs
51
+ Rake::GemPackageTask.new(spec) do |pkg|
52
+ pkg.gem_spec = spec
53
+
54
+ # Generate the gemspec file for github.
55
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
56
+ File.open(file, "w") {|f| f << spec.to_ruby }
57
+ end
58
+
59
+ # Generate documentation
60
+ Rake::RDocTask.new do |rd|
61
+ rd.main = "README"
62
+ rd.rdoc_files.include("README", "lib/**/*.rb")
63
+ rd.rdoc_dir = "rdoc"
64
+ end
65
+
66
+ desc 'Clear out RDoc and generated packages'
67
+ task :clean => [:clobber_rdoc, :clobber_package] do
68
+ rm "#{spec.name}.gemspec"
69
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module Procrastinate; end
3
+
4
+ require 'procrastinate/runtime'
5
+ require 'procrastinate/lock'
6
+ require 'procrastinate/dispatch_strategies'
7
+ require 'procrastinate/tasks'
8
+ require 'procrastinate/proxy'
9
+ require 'procrastinate/dispatcher'
10
+ require 'procrastinate/scheduler'
@@ -0,0 +1,9 @@
1
+
2
+ module Procrastinate::DispatchStrategy
3
+ # Raised when you request a shutdown and then schedule new work.
4
+ #
5
+ class ShutdownRequested < StandardError; end
6
+ end
7
+
8
+ require 'procrastinate/dispatch_strategy/simple'
9
+ require 'procrastinate/dispatch_strategy/throttled'
@@ -0,0 +1,47 @@
1
+
2
+ require 'thread'
3
+
4
+ class Procrastinate::DispatchStrategy::Simple
5
+ attr_reader :queue
6
+
7
+ # Client thread
8
+ def initialize
9
+ @queue = Queue.new
10
+ @shutdown_requested = false
11
+ end
12
+
13
+ def shutdown_requested?
14
+ @shutdown_requested
15
+ end
16
+
17
+ # Client thread
18
+ def schedule(task)
19
+ raise ::ShutdownRequested if shutdown_requested?
20
+
21
+ queue.push task
22
+ end
23
+
24
+ # Dispatcher thread
25
+ def spawn_new_workers(dispatcher)
26
+ # Spawn tasks
27
+ spawn(dispatcher) while should_spawn?
28
+
29
+ # If the queue is empty now, maybe shutdown the dispatcher
30
+ dispatcher.request_stop if shutdown_requested? && queue.empty?
31
+ end
32
+
33
+
34
+ # Spawn a new task from the job queue.
35
+ # Dispatcher thread
36
+ #
37
+ def spawn(dispatcher, &block)
38
+ dispatcher.spawn(queue.pop, &block)
39
+ end
40
+ def should_spawn?
41
+ not queue.empty?
42
+ end
43
+
44
+ def shutdown
45
+ @shutdown_requested = true
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+
2
+ # A dispatcher strategy that throttles tasks starting and ensures that no
3
+ # more than limit processes run concurrently.
4
+ #
5
+ class Procrastinate::DispatchStrategy::Throttled < Procrastinate::DispatchStrategy::Simple
6
+ attr_reader :limit, :current
7
+
8
+ # Client thread
9
+ def initialize(limit)
10
+ super()
11
+
12
+ @limit = limit
13
+ @current = 0
14
+ end
15
+
16
+ # Dispatcher thread
17
+ def spawn(dispatcher)
18
+ super(dispatcher) { @current -= 1 }
19
+ @current += 1
20
+ end
21
+ def should_spawn?
22
+ super &&
23
+ current < limit
24
+ end
25
+ end
@@ -0,0 +1,164 @@
1
+
2
+ # Dispatches and handles tasks and task completion. Only low level unixy
3
+ # manipulation here, no strategy. The only method you should call from the
4
+ # outside is #wakeup.
5
+ #
6
+ class Procrastinate::Dispatcher
7
+ # The dispatcher runs in its own thread, which sleeps most of the time.
8
+ attr_reader :thread
9
+
10
+ # This pipe is used to wait for events in the master process.
11
+ attr_reader :control_pipe
12
+
13
+ # A hash of <pid, callback> that contains callbacks for all the child
14
+ # processes we spawn. Once the process is complete, the callback is called
15
+ # in the dispatcher/strategy's thread.
16
+ attr_reader :handlers
17
+
18
+ # The strategy for dispatching new tasks. Makes all the decisions about
19
+ # when to launch what process.
20
+ #
21
+ attr_reader :strategy
22
+
23
+ def initialize(strategy)
24
+ @strategy = strategy
25
+
26
+ @control_pipe = IO.pipe
27
+ @handlers = {}
28
+ @stop_requested = false
29
+ end
30
+
31
+ def self.start(strategy)
32
+ new(strategy).tap do |dispatcher|
33
+ dispatcher.start
34
+ end
35
+ end
36
+ def start
37
+ register_signals
38
+ start_thread
39
+ end
40
+
41
+ # Called from anywhere, will complete all running tasks and stop the
42
+ # dispatcher.
43
+ #
44
+ def stop
45
+ request_stop
46
+ join
47
+ unregister_signals
48
+ end
49
+
50
+ # Called from the dispatcher thread, will cause the dispatcher to wait on
51
+ # all running tasks and then stop dispatching.
52
+ #
53
+ def request_stop
54
+ @stop_requested = true
55
+ wakeup
56
+ end
57
+
58
+ def stop_requested?
59
+ @stop_requested
60
+ end
61
+
62
+ def register_signals
63
+ trap('CHLD') { wakeup }
64
+ end
65
+ def unregister_signals
66
+ trap('CHLD', 'DEFAULT')
67
+ end
68
+
69
+ def start_thread
70
+ @thread = Thread.new do
71
+ Thread.current.abort_on_exception = true
72
+
73
+ # Loop until someone requests a shutdown.
74
+ loop do
75
+ wait_for_event
76
+ reap_workers
77
+
78
+ break if stop_requested?
79
+
80
+ strategy.spawn_new_workers(self)
81
+ end
82
+
83
+ wait_for_all_childs
84
+ end
85
+ end
86
+
87
+ def wait_for_event
88
+ # Returns array<ready_for_read, ..., ...>
89
+ IO.select([control_pipe.first], nil, nil)
90
+
91
+ # Consume the data (not important)
92
+ control_pipe.first.read_nonblock(1024)
93
+ rescue Errno::EAGAIN, Errno::EINTR
94
+ end
95
+
96
+ # Wake up the dispatcher thread.
97
+ #
98
+ def wakeup
99
+ control_pipe.last.write '.'
100
+ # rescue IOError
101
+ # Ignore:
102
+ end
103
+
104
+ # Waits until the dispatcher completes its work. If you don't initiate a
105
+ # shutdown, this may be forever.
106
+ #
107
+ def join
108
+ @thread.join
109
+ end
110
+
111
+ # Calls completion handlers for all the childs that have now exited.
112
+ #
113
+ def reap_workers
114
+ loop do
115
+ child_pid, status = Process.waitpid2(-1, Process::WNOHANG)
116
+ break unless child_pid
117
+
118
+ # Trigger the completion callback
119
+ handler = handlers.delete(child_pid)
120
+ handler.call if handler
121
+ end
122
+ rescue Errno::ECHILD
123
+ # Ignored: Child status has been reaped by someone else
124
+ end
125
+
126
+ # Spawns a process to work on +task+. If a block is given, it is called
127
+ # when the task completes.
128
+ #
129
+ # Example:
130
+ #
131
+ # spawn(wi) { puts "Task is complete" }
132
+ #
133
+ def spawn(task, &completion_handler)
134
+ pid = fork do
135
+ cleanup
136
+
137
+ task.run
138
+
139
+ exit! # this seems to be needed to avoid rspecs cleanup tasks
140
+ end
141
+
142
+ handlers[pid] = completion_handler
143
+ end
144
+
145
+ # Gets executed in child process to clean up file handles and pipes that the
146
+ # master holds.
147
+ #
148
+ def cleanup
149
+ # Children dont need the parents signal handler
150
+ trap(:CHLD, 'DEFAULT')
151
+
152
+ # The child doesn't need the control pipe for now.
153
+ control_pipe.each { |io| io.close }
154
+ end
155
+
156
+ # Waits for all childs to complete.
157
+ #
158
+ def wait_for_all_childs
159
+ until handlers.empty?
160
+ sleep 0.01
161
+ reap_workers
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,34 @@
1
+
2
+ # A file based lock below a base directory that is identified by Lock.base.
3
+ #
4
+ class Procrastinate::Lock
5
+ class << self
6
+ # Base directory for all lock files that are created.
7
+ #
8
+ attr_accessor :base
9
+ end
10
+
11
+ attr_reader :name # name of the lock
12
+ attr_reader :file # file handle of the lock
13
+ def initialize(name)
14
+ @name = name
15
+ @file = File.open(
16
+ File.join(
17
+ self.class.base, name),
18
+ 'w+')
19
+ end
20
+
21
+ def acquire
22
+ file.flock File::LOCK_EX
23
+ end
24
+ def release
25
+ file.flock File::LOCK_UN
26
+ end
27
+
28
+ def synchronize
29
+ acquire
30
+ yield
31
+ ensure
32
+ release
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+
2
+ class Procrastinate::Proxy
3
+ def initialize(worker, scheduler)
4
+ @worker = worker
5
+ @scheduler = scheduler
6
+ end
7
+
8
+ def respond_to?(name)
9
+ @worker.respond_to?(name)
10
+ end
11
+
12
+ def method_missing(name, *args, &block)
13
+ if respond_to? name
14
+ @scheduler.schedule(
15
+ Procrastinate::Task::MethodCall.new(@worker, name, args, block))
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+
2
+ # An instance of this class obtained from the scheduler can be used to perform
3
+ # synchronisation and other communication with the scheduler.
4
+ #
5
+ class Procrastinate::Runtime
6
+ def lock(name)
7
+ lock = Procrastinate::Lock.new(name)
8
+
9
+ lock.synchronize do
10
+ yield
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ class Procrastinate::Scheduler
2
+ attr_reader :dispatcher
3
+ attr_reader :strategy
4
+
5
+ def initialize
6
+ @shutdown_requested = false
7
+ end
8
+
9
+ # Start a new scheduler
10
+ def self.start(strategy=nil)
11
+ new.start(strategy)
12
+ end
13
+ def start(strategy=nil)
14
+ @strategy = strategy || Procrastinate::DispatchStrategy::Simple.new
15
+ @dispatcher = Procrastinate::Dispatcher.start(@strategy)
16
+
17
+ self
18
+ end
19
+
20
+ def create_proxy(worker)
21
+ return Procrastinate::Proxy.new(worker, self)
22
+ end
23
+
24
+ # Returns a runtime linked to this scheduler.
25
+ #
26
+ def runtime
27
+ Procrastinate::Runtime.new
28
+ end
29
+
30
+ # Called by the proxy to schedule work.
31
+ #
32
+ def schedule(task)
33
+ strategy.schedule(task)
34
+ dispatcher.wakeup
35
+ end
36
+
37
+ def shutdown
38
+ strategy.shutdown
39
+ dispatcher.join
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ module Procrastinate::Task
2
+ # Constructs an object of type +klass+ and calls a method on it.
3
+ #
4
+ class MethodCall
5
+ def initialize(instance, method, arguments, block)
6
+ @instance = instance
7
+ @method = method
8
+ @arguments = arguments
9
+ @block = block
10
+ end
11
+
12
+ def run
13
+ @instance.send(@method, *@arguments, &@block)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: procrastinate
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Kaspar Schiess
13
+ - Patrick Marchi
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-10 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: flexmock
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ version_requirements: *id002
47
+ description:
48
+ email:
49
+ - kaspar.schiess@absurd.li
50
+ - mail@patrickmarchi.ch
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README
57
+ files:
58
+ - LICENSE
59
+ - Rakefile
60
+ - README
61
+ - lib/procrastinate/dispatch_strategies.rb
62
+ - lib/procrastinate/dispatch_strategy/simple.rb
63
+ - lib/procrastinate/dispatch_strategy/throttled.rb
64
+ - lib/procrastinate/dispatcher.rb
65
+ - lib/procrastinate/lock.rb
66
+ - lib/procrastinate/proxy.rb
67
+ - lib/procrastinate/runtime.rb
68
+ - lib/procrastinate/scheduler.rb
69
+ - lib/procrastinate/tasks.rb
70
+ - lib/procrastinate.rb
71
+ has_rdoc: true
72
+ homepage: http://github.com/kschiess/procrastinate
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options:
77
+ - --main
78
+ - README
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ requirements: []
98
+
99
+ rubyforge_project:
100
+ rubygems_version: 1.3.7
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Framework to run tasks in separate processes.
104
+ test_files: []
105
+