future_agent 0.1

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.
@@ -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
+