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.
- data/README.rdoc +32 -0
- data/common_test_cases +11 -0
- data/lib/future_agent/future_agent.rb +143 -0
- data/spec/future_agent/future_agent_spec.rb +26 -0
- data/spec/spec_helper.rb +3 -0
- metadata +69 -0
data/README.rdoc
ADDED
@@ -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
|
+
|
data/common_test_cases
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
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
|
+
|