future_agent 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ = FutureAgent
2
+
3
+ One of the things of Clojure that appeal to me are the agents. Agents in Clojure
4
+ are functions that process their body asynchronously in a separate thread.
5
+ If you try to read the result of an agent before it completes, it blocks until
6
+ the result is available. Otherwise the parallelism is transparent for the
7
+ enduser.
8
+
9
+ This library tries to emulate that behaviour to a certain degree; since
10
+ threading isn't what it could be under ruby (GIL, reentrant libraries, ... )
11
+ I decided to work with processes instead of threads for this library.
12
+ Since most modern unices use COW forking, doing multi-processing instead of multi-threading probably isn't as painful performance and memory wise as you might imagine.
13
+
14
+ Not heavily tested at the moment, any feedback would be welcome.
15
+
16
+ == Signal Handlers
17
+ FutureAgent installs a SIGCHLD/SIGCLD handler when the class is loaded. It tries to hand off to the previous signal handler installed for the SIGCHLD/SIGCLD signals, if one was installed (caveat: handlers in classes or modules are not yet supported).
18
+
19
+ This means that if you install your own SIGCLD handler after requiring FutureAgent, you have to take care to hand off any childern notifications of terminated agents to FutureAgent.handle_pid, otherwise you'll risk zombies and memory leaks (because the Zombies will eat your computes brains).
20
+
21
+ == Usage
22
+ require 'future_agent/future_agent'
23
+
24
+ agent = FutureAgent.fork { compute_value() }
25
+ # do other stuff
26
+ begin
27
+ agent.result # will block if compute_value is not done yet,
28
+ # returns the result of compute_value()
29
+ rescue FutureAgent::ChildDied
30
+ # compute_value() raised an exception. deal with it here
31
+ end
32
+
@@ -0,0 +1,11 @@
1
+ require './lib/future_agent/future_agent'
2
+
3
+ fa = FutureAgent.fork { :foo }
4
+ fa.result
5
+
6
+ fa = FutureAgent.fork { nil }
7
+ fa.result
8
+
9
+
10
+ fa = FutureAgent.fork { raise }
11
+ fa.result
@@ -0,0 +1,143 @@
1
+ class FutureAgent
2
+ class ChildDied < StandardError; end
3
+
4
+ @agents = {}
5
+
6
+ =begin rdoc
7
+ Installs the SIGCHLD/SIGCLD signal handler for FutureAgent. Usually this
8
+ will not need to be invoked manually; it is invoked when the class is required.
9
+ If you overwrite the SIGCHLD handler somewhere after requiring this class
10
+ and you want to reset the default behaviour, you can call this method.
11
+
12
+ Caution: The signal handler will try to call the previously installed
13
+ handler for non-agent children that die. You can probably build a handler
14
+ invocation cycle if you aren't careful.
15
+ =end
16
+ def self.setup_signal_handler
17
+ @old_handler = Signal.trap( "SIGCLD" ) { |signo| child_handler( signo ) }
18
+ end
19
+
20
+ =begin rdoc
21
+ Handles the death of a signel agent child identified by the PID.
22
+ Usually invoked automatically by the FutureAgent SIGCHLD/SIGCLD handler.
23
+ If you overwrite this handler with your own however, you will need to
24
+ call this method to clean up the agent state if an agent dies.
25
+ =end
26
+ def self.handle_pid( signal_number, pid )
27
+ unless @agents.has_key?( pid )
28
+ call_original_child_handler signal_number
29
+ return
30
+ end
31
+
32
+ agent = @agents.delete(pid)
33
+ agent.result if( $?.success? )
34
+ end
35
+
36
+
37
+ setup_signal_handler
38
+
39
+ =begin rdoc
40
+ Returns a new agent instances that has been forked and is already running.
41
+ Retrieve the result from this agent by calling agent.result
42
+ =end
43
+ def self.fork( &block )
44
+ agent = new( &block )
45
+ child_pid = agent.send :fork!
46
+ @agents[child_pid] = agent
47
+ agent
48
+ end
49
+
50
+ def initialize( &block ) # :nodoc:
51
+ @async_block = block
52
+ end
53
+
54
+ =begin rdoc
55
+ Returns the result of the agent. If the result isn't available yet, calling
56
+ this method will block until the agent is done.
57
+
58
+ Calling this method will raise a FutureAgent::ChildDied exception if the
59
+ child process died with an exit status other than 0 (usually due to a raised
60
+ exception)
61
+ =end
62
+ def result
63
+ return @result if defined?( @result )
64
+
65
+ begin
66
+ read_result
67
+ ensure
68
+ @read_pipe.close
69
+ end
70
+ end
71
+
72
+ protected
73
+ def self.child_handler( signal_number )
74
+ # since signals can get lost of too many are triggered in too short a time,
75
+ # we will have to loop over Process.wait until no more children can be
76
+ # reaped, i.e. Errno::ECHILD is raised
77
+ loop do
78
+
79
+ begin
80
+ exited_child_pid = Process.wait
81
+ handle_pid( signal_number, exited_child_pid )
82
+
83
+ rescue Errno::ECHILD
84
+ break
85
+ end
86
+
87
+ end
88
+ end
89
+
90
+ def self.call_original_child_handler( signal_number )
91
+ return unless @old_handler
92
+
93
+ case @old_handler
94
+ when String, Symbol
95
+ #TODO: deal with handlers in classes
96
+ Kernel.send @old_handler, signal_number
97
+
98
+ when Proc
99
+ @old_handler.call( signal_number )
100
+ end
101
+ end
102
+
103
+ def read_result
104
+ begin
105
+ @result = Marshal.load( @read_pipe.read )
106
+ rescue ArgumentError
107
+ # the write pipe closed without writing any data; this means the
108
+ # other side died somewhere
109
+ @result = nil
110
+ raise FutureAgent::ChildDied
111
+ end
112
+ end
113
+
114
+ def fork!
115
+ @read_pipe, @write_pipe = IO.pipe
116
+ retval = fork
117
+
118
+ if( retval ) # parent
119
+ @write_pipe.close
120
+ return retval
121
+
122
+ else # child
123
+ @read_pipe.close
124
+ process_block
125
+ exit
126
+ end
127
+ end
128
+
129
+ def process_block # child method
130
+ begin
131
+ result = @async_block.call
132
+ rescue Exception
133
+ #TODO: How do we handle fatal errors/exceptions for the child?
134
+ # Some debug logging would be nice... Push this task to the
135
+ # user of this class for now
136
+ exit( -1 )
137
+ else
138
+ Marshal.dump( result, @write_pipe )
139
+ ensure
140
+ @write_pipe.close
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe FutureAgent do
4
+ describe ".fork" do
5
+ it "should return nil if the block gives nil" do
6
+ fa = FutureAgent.fork { nil }
7
+ fa.result.should be_nil
8
+ end
9
+
10
+ it "should return :foo if the block gives :foo" do
11
+ fa = FutureAgent.fork { :foo }
12
+ fa.result.should == :foo
13
+ end
14
+
15
+ it "should return a Time instance if the block gives Time.now" do
16
+ fa = FutureAgent.fork { Time.now }
17
+ fa.result.should be_a( Time )
18
+ end
19
+
20
+ it "should raise FutureAgent::ChildDied if the block raises an exception" do
21
+ fa = FutureAgent.fork { raise }
22
+ lambda { fa.result }.should raise_error( FutureAgent::ChildDied )
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,3 @@
1
+ $: << File.expand_path( File.join( '..', 'lib' ), __FILE__ )
2
+ require 'future_agent/future_agent'
3
+
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: future_agent
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Sven Riedel
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-07-17 00:00:00 +02:00
17
+ default_executable:
18
+ dependencies: []
19
+
20
+ description: Compute values asynchronously as seen with Clojure agents, but uses multi-processing instead of multi-threading.
21
+ email: sr@gimp.org
22
+ executables: []
23
+
24
+ extensions: []
25
+
26
+ extra_rdoc_files:
27
+ - README.rdoc
28
+ files:
29
+ - README.rdoc
30
+ - lib/future_agent/future_agent.rb
31
+ - spec/spec_helper.rb
32
+ - spec/future_agent/future_agent_spec.rb
33
+ - common_test_cases
34
+ has_rdoc: true
35
+ homepage:
36
+ licenses: []
37
+
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --charset=UTF-8
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ segments:
49
+ - 1
50
+ - 8
51
+ - 7
52
+ version: 1.8.7
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.7
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Computes values asynchronously.
68
+ test_files: []
69
+